Repository navigation
Add the @Present constraint for JsonNullable #208
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| invoker.goals=clean verify |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,90 @@ | ||||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||||
| <project xmlns="http://maven.apache.org/POM/4.0.0" | ||||||
| xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||||||
| xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> | ||||||
| <modelVersion>4.0.0</modelVersion> | ||||||
|
|
||||||
| <groupId>org.openapitools.it</groupId> | ||||||
| <artifactId>test</artifactId> | ||||||
| <version>1.0-SNAPSHOT</version> | ||||||
| <packaging>jar</packaging> | ||||||
|
|
||||||
| <properties> | ||||||
| <java.version>8</java.version> | ||||||
| <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> | ||||||
| </properties> | ||||||
|
|
||||||
| <dependencies> | ||||||
| <dependency> | ||||||
| <groupId>org.openapitools</groupId> | ||||||
| <artifactId>jackson-databind-nullable</artifactId> | ||||||
| <version>@project.version@</version> | ||||||
| </dependency> | ||||||
| <dependency> | ||||||
| <groupId>javax.validation</groupId> | ||||||
| <artifactId>validation-api</artifactId> | ||||||
| <version>2.0.1.Final</version> | ||||||
| </dependency> | ||||||
| <dependency> | ||||||
| <groupId>org.hibernate.validator</groupId> | ||||||
| <artifactId>hibernate-validator</artifactId> | ||||||
| <version>6.2.5.Final</version> | ||||||
| </dependency> | ||||||
| <dependency> | ||||||
| <groupId>com.fasterxml.jackson.core</groupId> | ||||||
| <artifactId>jackson-databind</artifactId> | ||||||
| <version>2.21.6</version> | ||||||
| </dependency> | ||||||
| </dependencies> | ||||||
|
|
||||||
| <build> | ||||||
| <plugins> | ||||||
| <plugin> | ||||||
| <artifactId>maven-compiler-plugin</artifactId> | ||||||
| <version>3.14.1</version> | ||||||
| <configuration> | ||||||
| <release>${java.version}</release> | ||||||
| </configuration> | ||||||
| </plugin> | ||||||
| <plugin> | ||||||
| <groupId>org.apache.maven.plugins</groupId> | ||||||
| <artifactId>maven-dependency-plugin</artifactId> | ||||||
| <version>3.6.0</version> | ||||||
| <executions> | ||||||
| <execution> | ||||||
| <id>copy-dependencies</id> | ||||||
| <phase>prepare-package</phase> | ||||||
| <goals> | ||||||
| <goal>copy-dependencies</goal> | ||||||
| </goals> | ||||||
| <configuration> | ||||||
| <outputDirectory>${project.build.directory}/lib</outputDirectory> | ||||||
| <overWriteReleases>false</overWriteReleases> | ||||||
| <overWriteSnapshots>false</overWriteSnapshots> | ||||||
| <overWriteIfNewer>true</overWriteIfNewer> | ||||||
| </configuration> | ||||||
| </execution> | ||||||
| </executions> | ||||||
| </plugin> | ||||||
| <plugin> | ||||||
| <groupId>org.codehaus.mojo</groupId> | ||||||
| <artifactId>exec-maven-plugin</artifactId> | ||||||
| <version>3.6.2</version> | ||||||
| <executions> | ||||||
| <execution> | ||||||
| <id>run</id> | ||||||
| <phase>verify</phase> | ||||||
| <goals> | ||||||
| <goal>exec</goal> | ||||||
| </goals> | ||||||
| <configuration> | ||||||
| <executable>java</executable> | ||||||
| <commandlineArgs>--class-path ${project.build.directory}/test-1.0-SNAPSHOT.jar${path.separator}${project.build.directory}/lib/* org.openapitools.it.Main</commandlineArgs> | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: This IT targets Java 8 ( Prompt for AI agents
Suggested change
|
||||||
| </configuration> | ||||||
| </execution> | ||||||
| </executions> | ||||||
| </plugin> | ||||||
| </plugins> | ||||||
| </build> | ||||||
|
|
||||||
| </project> | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,205 @@ | ||
| package org.openapitools.it; | ||
|
|
||
| import org.hibernate.validator.messageinterpolation.ParameterMessageInterpolator; | ||
| import org.openapitools.jackson.nullable.JsonNullable; | ||
| import org.openapitools.jackson.nullable.JsonNullableValueExtractor; | ||
| import org.openapitools.jackson.nullable.javax.Present; | ||
|
|
||
| import javax.validation.ConstraintViolation; | ||
| import javax.validation.UnexpectedTypeException; | ||
| import javax.validation.Validation; | ||
| import javax.validation.Validator; | ||
| import javax.validation.ValidatorFactory; | ||
| import javax.validation.constraints.Size; | ||
| import javax.validation.groups.Default; | ||
| import javax.validation.valueextraction.Unwrapping; | ||
| import javax.validation.valueextraction.ValueExtractor; | ||
| import java.lang.reflect.Constructor; | ||
| import java.util.Set; | ||
| import java.util.concurrent.Callable; | ||
|
|
||
| public class Main { | ||
|
|
||
| private static int failures; | ||
|
|
||
| public static void main(String[] args) throws Exception { | ||
| tryLoadingJavaxValidation(); | ||
|
|
||
| ValidatorFactory factory = Validation.byDefaultProvider() | ||
| .configure() | ||
| .messageInterpolator(new ParameterMessageInterpolator()) | ||
| .buildValidatorFactory(); | ||
| Validator validator = factory.getValidator(); | ||
|
|
||
| // the existing javax value extractor unwraps JsonNullable | ||
| check("extractor: undefined is not validated", violations(validator, new Sized(JsonNullable.<String>undefined())) == 0); | ||
| check("extractor: null is not validated", violations(validator, new Sized(JsonNullable.<String>of(null))) == 0); | ||
| check("extractor: short value is valid", violations(validator, new Sized(JsonNullable.of("abc"))) == 0); | ||
| check("extractor: long value is unwrapped and rejected", violations(validator, new Sized(JsonNullable.of("too long"))) == 1); | ||
|
|
||
| // @Present on a field | ||
| check("field: undefined is invalid", violations(validator, new OnField(JsonNullable.<String>undefined())) == 1); | ||
| check("field: explicit null is valid", violations(validator, new OnField(JsonNullable.<String>of(null))) == 0); | ||
| check("field: value is valid", violations(validator, new OnField(JsonNullable.of("red"))) == 0); | ||
| check("field: null reference is invalid", violations(validator, new OnField(null)) == 1); | ||
| check("field: default message", "must be present".equals( | ||
| firstMessage(validator, new OnField(JsonNullable.<String>undefined())))); | ||
|
|
||
| // on a getter | ||
| check("getter: undefined is invalid", violations(validator, new OnGetter(JsonNullable.<String>undefined())) == 1); | ||
| check("getter: explicit null is valid", violations(validator, new OnGetter(JsonNullable.<String>of(null))) == 0); | ||
| check("getter: value is valid", violations(validator, new OnGetter(JsonNullable.of("red"))) == 0); | ||
| check("getter: null reference is invalid", violations(validator, new OnGetter(null)) == 1); | ||
|
|
||
| // on a constructor parameter | ||
| Constructor<OnConstructor> constructor = OnConstructor.class.getConstructor(JsonNullable.class); | ||
| check("constructor: undefined is invalid", validator.forExecutables().validateConstructorParameters( | ||
| constructor, new Object[]{JsonNullable.undefined()}).size() == 1); | ||
| check("constructor: explicit null is valid", validator.forExecutables().validateConstructorParameters( | ||
| constructor, new Object[]{JsonNullable.of(null)}).isEmpty()); | ||
| check("constructor: value is valid", validator.forExecutables().validateConstructorParameters( | ||
| constructor, new Object[]{JsonNullable.of("red")}).isEmpty()); | ||
|
|
||
| // groups, repetition, message | ||
| OnGroup onGroup = new OnGroup(JsonNullable.<String>undefined()); | ||
| check("group: not applied in the default group", validator.validate(onGroup).isEmpty()); | ||
| check("group: applied in its own group", validator.validate(onGroup, Strict.class).size() == 1); | ||
| OnRepeated onRepeated = new OnRepeated(JsonNullable.<String>undefined()); | ||
| check("repeated: one violation per group", validator.validate(onRepeated).size() == 1 | ||
| && validator.validate(onRepeated, Default.class, Strict.class).size() == 2); | ||
| check("message: custom message", "color is required".equals( | ||
| firstMessage(validator, new OnMessage(JsonNullable.<String>undefined())))); | ||
|
|
||
| // without the payload the wrapper is never offered to the validator | ||
| check("no payload: undefined passes silently", violations(validator, new WithoutSkip(JsonNullable.<String>undefined())) == 0); | ||
| check("no payload: null reference passes silently", violations(validator, new WithoutSkip(null)) == 0); | ||
| check("no payload: explicit null is refused", refused(validator, new WithoutSkip(JsonNullable.<String>of(null)))); | ||
| check("no payload: value is refused", refused(validator, new WithoutSkip(JsonNullable.of("red")))); | ||
|
|
||
| factory.close(); | ||
| if (failures > 0) { | ||
| System.out.println(failures + " check(s) failed"); | ||
| System.exit(1); | ||
| } | ||
| System.out.println("Success"); | ||
| } | ||
|
|
||
| private static int violations(Validator validator, Object bean) { | ||
| Set<ConstraintViolation<Object>> violations = validator.validate(bean); | ||
| return violations.size(); | ||
| } | ||
|
|
||
| private static String firstMessage(Validator validator, Object bean) { | ||
| Set<ConstraintViolation<Object>> violations = validator.validate(bean); | ||
| return violations.isEmpty() ? null : violations.iterator().next().getMessage(); | ||
| } | ||
|
|
||
| private static boolean refused(final Validator validator, final Object bean) throws Exception { | ||
| Callable<Object> validation = new Callable<Object>() { | ||
| @Override | ||
| public Object call() { | ||
| return validator.validate(bean); | ||
| } | ||
| }; | ||
| try { | ||
| validation.call(); | ||
| return false; | ||
| } catch (UnexpectedTypeException e) { | ||
| return e.getMessage().contains("HV000030"); | ||
| } | ||
| } | ||
|
|
||
| private static void check(String name, boolean ok) { | ||
| System.out.println((ok ? "ok " : "FAILED ") + name); | ||
| if (!ok) { | ||
| failures++; | ||
| } | ||
| } | ||
|
|
||
| private static void tryLoadingJavaxValidation() { | ||
| for (ValueExtractor<?> ve : java.util.ServiceLoader.load(ValueExtractor.class)) { | ||
| if (ve instanceof JsonNullableValueExtractor) { | ||
| System.out.println("Successfully loaded JsonNullableValueExtractor via ServiceLoader"); | ||
| return; | ||
| } | ||
| } | ||
| System.out.println("Failed to load JsonNullableValueExtractor via ServiceLoader"); | ||
| System.exit(1); | ||
| } | ||
|
|
||
| public interface Strict { | ||
| } | ||
|
|
||
| public static class Sized { | ||
| @Size(max = 5) | ||
| public JsonNullable<String> value; | ||
|
|
||
| public Sized(JsonNullable<String> value) { | ||
| this.value = value; | ||
| } | ||
| } | ||
|
|
||
| public static class OnField { | ||
| @Present(payload = Unwrapping.Skip.class) | ||
| public JsonNullable<String> color; | ||
|
|
||
| public OnField(JsonNullable<String> color) { | ||
| this.color = color; | ||
| } | ||
| } | ||
|
|
||
| public static class OnGetter { | ||
| private final JsonNullable<String> color; | ||
|
|
||
| public OnGetter(JsonNullable<String> color) { | ||
| this.color = color; | ||
| } | ||
|
|
||
| @Present(payload = Unwrapping.Skip.class) | ||
| public JsonNullable<String> getColor() { | ||
| return color; | ||
| } | ||
| } | ||
|
|
||
| public static class OnConstructor { | ||
| public OnConstructor(@Present(payload = Unwrapping.Skip.class) JsonNullable<String> color) { | ||
| } | ||
| } | ||
|
|
||
| public static class OnGroup { | ||
| @Present(groups = Strict.class, payload = Unwrapping.Skip.class) | ||
| public JsonNullable<String> color; | ||
|
|
||
| public OnGroup(JsonNullable<String> color) { | ||
| this.color = color; | ||
| } | ||
| } | ||
|
|
||
| public static class OnRepeated { | ||
| @Present(payload = Unwrapping.Skip.class) | ||
| @Present(groups = Strict.class, payload = Unwrapping.Skip.class) | ||
| public JsonNullable<String> color; | ||
|
|
||
| public OnRepeated(JsonNullable<String> color) { | ||
| this.color = color; | ||
| } | ||
| } | ||
|
|
||
| public static class OnMessage { | ||
| @Present(message = "color is required", payload = Unwrapping.Skip.class) | ||
| public JsonNullable<String> color; | ||
|
|
||
| public OnMessage(JsonNullable<String> color) { | ||
| this.color = color; | ||
| } | ||
| } | ||
|
|
||
| public static class WithoutSkip { | ||
| @Present | ||
| public JsonNullable<String> color; | ||
|
|
||
| public WithoutSkip(JsonNullable<String> color) { | ||
| this.color = color; | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| package org.openapitools.jackson.nullable.jakarta; | ||
|
|
||
| import jakarta.validation.Constraint; | ||
| import jakarta.validation.Payload; | ||
|
|
||
| import java.lang.annotation.Documented; | ||
| import java.lang.annotation.Repeatable; | ||
| import java.lang.annotation.Retention; | ||
| import java.lang.annotation.Target; | ||
|
|
||
| import static java.lang.annotation.ElementType.ANNOTATION_TYPE; | ||
| import static java.lang.annotation.ElementType.CONSTRUCTOR; | ||
| import static java.lang.annotation.ElementType.FIELD; | ||
| import static java.lang.annotation.ElementType.METHOD; | ||
| import static java.lang.annotation.ElementType.PARAMETER; | ||
| import static java.lang.annotation.ElementType.TYPE_USE; | ||
| import static java.lang.annotation.RetentionPolicy.RUNTIME; | ||
|
|
||
| /** | ||
| * The annotated {@code JsonNullable} must be present, that is not undefined. An explicit {@code null} value is | ||
| * present and therefore valid; a {@code null} reference is not. | ||
| * <p> | ||
| * The value extractor of this module unwraps {@code JsonNullable} by default, so the constraint only sees the | ||
| * wrapper when unwrapping is skipped at the place of use: | ||
| * <pre> | ||
| * @Present(payload = Unwrapping.Skip.class) | ||
| * private JsonNullable<String> color = JsonNullable.undefined(); | ||
| * </pre> | ||
| */ | ||
| @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) | ||
| @Retention(RUNTIME) | ||
| @Repeatable(Present.List.class) | ||
| @Documented | ||
| @Constraint(validatedBy = { PresentValidator.class }) | ||
| public @interface Present { | ||
|
|
||
| String message() default "must be present"; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: The default Prompt for AI agents |
||
|
|
||
| Class<?>[] groups() default { }; | ||
|
|
||
| Class<? extends Payload>[] payload() default { }; | ||
|
|
||
| /** | ||
| * Defines several {@link Present} annotations on the same element. | ||
| */ | ||
| @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) | ||
| @Retention(RUNTIME) | ||
| @Documented | ||
| @interface List { | ||
|
|
||
| Present[] value(); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| package org.openapitools.jackson.nullable.jakarta; | ||
|
|
||
| import jakarta.validation.ConstraintValidator; | ||
| import jakarta.validation.ConstraintValidatorContext; | ||
| import org.openapitools.jackson.nullable.JsonNullable; | ||
|
|
||
| /** | ||
| * Validator for {@link Present} (modern jakarta-validation version) | ||
| */ | ||
| public class PresentValidator implements ConstraintValidator<Present, JsonNullable<?>> { | ||
| @Override | ||
| public boolean isValid(JsonNullable<?> value, ConstraintValidatorContext context) { | ||
| return value != null && value.isPresent(); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P3: The 'Without the payload...' sentence is a long run-on that stacks the pass-through case, the throw case, and the no-extractor case in one clause chain, which makes the most important caveat in this section hard to follow. Split it into separate sentences: '...an undefined property and a
nullreference pass unchecked. A defined value (an explicitnullincluded) makes Hibernate Validator throw anUnexpectedTypeException(HV000030). Without the extractor, a plain@Presentsimply checks presence.'Prompt for AI agents