Skip to content

Add the @Present constraint for JsonNullable - #208

Open
Sekator778 wants to merge 1 commit into
masterfrom
feature/present
Open

Sekator778 wants to merge 1 commit into
masterfrom
feature/present

Conversation

@Sekator778

@Sekator778 Sekator778 commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Adds @Present for #176: the JsonNullable must be set, so an undefined one is invalid and an explicit null is valid. It comes for jakarta.validation and javax.validation under the same name in two packages, and needs payload = Unwrapping.Skip.class where it is used, because the value extractor unwraps by default. The README says what happens without it.

@jpfinne the template's packages are org.openapitools.jackson.nullable.jakarta and .javax.

Closes #176


Summary by cubic

Adds a @Present constraint for JsonNullable that requires the property to be set: JsonNullable.undefined() and a null reference are invalid, while an explicit null (JsonNullable.of(null)) is valid. This is the opposite of @NotNull on a JsonNullable. Closes #176.

The annotation ships in both org.openapitools.jackson.nullable.jakarta and org.openapitools.jackson.nullable.javax, which are now exported in the module-info files. It supports fields, getters, constructor parameters, groups, and repetition, with a default message of "must be present".

Usage

  • Use @Present(payload = Unwrapping.Skip.class) at the use site. The module's value extractor unwraps JsonNullable by default, so without the payload an undefined property passes unchecked and a defined value throws UnexpectedTypeException.
  • In composed constraints the payload is ignored as a meta-annotation, so give it where the composed annotation is used.

Written for commit 7fa531d. Summary will update on new commits.

View guided diff Turn on auto-fix

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 issues found across 11 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/main/java/org/openapitools/jackson/nullable/jakarta/Present.java">

<violation number="1" location="src/main/java/org/openapitools/jackson/nullable/jakarta/Present.java:37">
P3: The default `message()` is a plain literal, so the default violation message cannot be overridden globally through `ValidationMessages.properties` (the standard Bean Validation mechanism). Users must spell out `message = "{...}"` at every use site to change or localize it. Publish a message key instead, e.g. `default "{org.openapitools.jackson.nullable.jakarta.Present.message}"`, with a matching `META-INF/ValidationMessages.properties` entry, following how `jakarta.validation.constraints.*` define their defaults. Applies to the javax variant too.</violation>
</file>

<file name="README.md">

<violation number="1" location="README.md:124">
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 `null` reference pass unchecked. A defined value (an explicit `null` included) makes Hibernate Validator throw an `UnexpectedTypeException` (HV000030). Without the extractor, a plain `@Present` simply checks presence.'</violation>
</file>

<file name="src/it/javax-classpath/pom.xml">

<violation number="1" location="src/it/javax-classpath/pom.xml:82">
P3: This IT targets Java 8 (`<java.version>8</java.version>` with `<release>8</release>`), but `java --class-path` is only recognized by the JDK 9+ launcher (JEP 293 long-form options; a JDK 8 java exits with "Unrecognized option: --class-path"). The sibling `jakarta-jar-classpath` IT pairs the same `--class-path` invocation with `java.version=9`, which is consistent. Since the javax flavor exists specifically to cover the Java 8 runtime, run this under a JDK 8 `java` and the verify phase fails. Use `-classpath` (valid on every JDK) so the Java 8 scenario is actually runnable; the current CI matrix (17/21/25) masks this.</violation>
</file>

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

@Constraint(validatedBy = { PresentValidator.class })
public @interface Present {

String message() default "must be present";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The default message() is a plain literal, so the default violation message cannot be overridden globally through ValidationMessages.properties (the standard Bean Validation mechanism). Users must spell out message = "{...}" at every use site to change or localize it. Publish a message key instead, e.g. default "{org.openapitools.jackson.nullable.jakarta.Present.message}", with a matching META-INF/ValidationMessages.properties entry, following how jakarta.validation.constraints.* define their defaults. Applies to the javax variant too.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At src/main/java/org/openapitools/jackson/nullable/jakarta/Present.java, line 37:

<comment>The default `message()` is a plain literal, so the default violation message cannot be overridden globally through `ValidationMessages.properties` (the standard Bean Validation mechanism). Users must spell out `message = "{...}"` at every use site to change or localize it. Publish a message key instead, e.g. `default "{org.openapitools.jackson.nullable.jakarta.Present.message}"`, with a matching `META-INF/ValidationMessages.properties` entry, following how `jakarta.validation.constraints.*` define their defaults. Applies to the javax variant too.</comment>

<file context>
@@ -0,0 +1,53 @@
+@Constraint(validatedBy = { PresentValidator.class })
+public @interface Present {
+
+    String message() default "must be present";
+
+    Class<?>[] groups() default { };
</file context>

Comment thread README.md
public JsonNullable<String> color = JsonNullable.undefined();
```
The annotation comes in two packages with the same name, `org.openapitools.jackson.nullable.jakarta` for `jakarta.validation` and `org.openapitools.jackson.nullable.javax` for `javax.validation`.
Without the payload, while the module's `ValueExtractor` is registered (the default on the class path), an undefined property and a `null` reference pass unchecked and a defined value, an explicit `null` included, makes Hibernate Validator throw an `UnexpectedTypeException` (HV000030); without the extractor a plain `@Present` simply checks presence. This was measured on Hibernate Validator 6.2.5, 7.0.5, 8.0.2, 9.0.1 and 9.1.4.

Copy link
Copy Markdown

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 null reference pass unchecked. A defined value (an explicit null included) makes Hibernate Validator throw an UnexpectedTypeException (HV000030). Without the extractor, a plain @Present simply checks presence.'

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At README.md, line 124:

<comment>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 `null` reference pass unchecked. A defined value (an explicit `null` included) makes Hibernate Validator throw an `UnexpectedTypeException` (HV000030). Without the extractor, a plain `@Present` simply checks presence.'</comment>

<file context>
@@ -108,6 +108,22 @@ Set<ConstraintViolation<Pet>> validationResult = validator.validate(myPet);
+public JsonNullable<String> color = JsonNullable.undefined();
+```
+The annotation comes in two packages with the same name, `org.openapitools.jackson.nullable.jakarta` for `jakarta.validation` and `org.openapitools.jackson.nullable.javax` for `javax.validation`.
+Without the payload, while the module's `ValueExtractor` is registered (the default on the class path), an undefined property and a `null` reference pass unchecked and a defined value, an explicit `null` included, makes Hibernate Validator throw an `UnexpectedTypeException` (HV000030); without the extractor a plain `@Present` simply checks presence. This was measured on Hibernate Validator 6.2.5, 7.0.5, 8.0.2, 9.0.1 and 9.1.4.
+In a composed constraint that carries `@Present(payload = Unwrapping.Skip.class)` as a meta-annotation the payload is ignored and an undefined property passes; give the payload where the composed annotation is used.
+
</file context>
Suggested change
Without the payload, while the module's `ValueExtractor` is registered (the default on the class path), an undefined property and a `null` reference pass unchecked and a defined value, an explicit `null` included, makes Hibernate Validator throw an `UnexpectedTypeException` (HV000030); without the extractor a plain `@Present` simply checks presence. This was measured on Hibernate Validator 6.2.5, 7.0.5, 8.0.2, 9.0.1 and 9.1.4.
Without the payload, while the module's `ValueExtractor` is registered (the default on the class path), an undefined property and a `null` reference pass unchecked. A defined value (an explicit `null` included) makes Hibernate Validator throw an `UnexpectedTypeException` (HV000030). Without the extractor a plain `@Present` simply checks presence. This was measured on Hibernate Validator 6.2.5, 7.0.5, 8.0.2, 9.0.1 and 9.1.4.

</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>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: This IT targets Java 8 (<java.version>8</java.version> with <release>8</release>), but java --class-path is only recognized by the JDK 9+ launcher (JEP 293 long-form options; a JDK 8 java exits with "Unrecognized option: --class-path"). The sibling jakarta-jar-classpath IT pairs the same --class-path invocation with java.version=9, which is consistent. Since the javax flavor exists specifically to cover the Java 8 runtime, run this under a JDK 8 java and the verify phase fails. Use -classpath (valid on every JDK) so the Java 8 scenario is actually runnable; the current CI matrix (17/21/25) masks this.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At src/it/javax-classpath/pom.xml, line 82:

<comment>This IT targets Java 8 (`<java.version>8</java.version>` with `<release>8</release>`), but `java --class-path` is only recognized by the JDK 9+ launcher (JEP 293 long-form options; a JDK 8 java exits with "Unrecognized option: --class-path"). The sibling `jakarta-jar-classpath` IT pairs the same `--class-path` invocation with `java.version=9`, which is consistent. Since the javax flavor exists specifically to cover the Java 8 runtime, run this under a JDK 8 `java` and the verify phase fails. Use `-classpath` (valid on every JDK) so the Java 8 scenario is actually runnable; the current CI matrix (17/21/25) masks this.</comment>

<file context>
@@ -0,0 +1,90 @@
+                        </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>
+                        </configuration>
+                    </execution>
</file context>
Suggested change
<commandlineArgs>--class-path ${project.build.directory}/test-1.0-SNAPSHOT.jar${path.separator}${project.build.directory}/lib/* org.openapitools.it.Main</commandlineArgs>
+ <commandlineArgs>-classpath ${project.build.directory}/test-1.0-SNAPSHOT.jar${path.separator}${project.build.directory}/lib/* org.openapitools.it.Main</commandlineArgs>

@Thorsrud22 Thorsrud22 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ran clean verify -P integration-test on JDK 17 and 21 with this merged onto master: 381 tests and all four ITs pass, and the new javax-classpath IT covers field, getter, constructor, the null reference and the default message. The opt-in payload is the only option, since the spec requires payload() to default to an empty array, and the README is clear about what happens without it.

One optional gap: the new exports in both module-info files aren't exercised by the module-path ITs (jakarta-jar-modulepath, jakarta-jlink don't use @Present). One @Present check in jakarta-jar-modulepath would cover Hibernate Validator instantiating PresentValidator from the module. Fine as a follow-up. LGTM.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Req] add javax/jakarta validator for Present

2 participants