Skip to content

[JAVA] [Spring] JSpecify, fix nullable + required field - #24711

Open
jpfinne wants to merge 28 commits into
OpenAPITools:masterfrom
jpfinne:bug/nullableRequired
Open

[JAVA] [Spring] JSpecify, fix nullable + required field#24711
jpfinne wants to merge 28 commits into
OpenAPITools:masterfrom
jpfinne:bug/nullableRequired

Conversation

@jpfinne

@jpfinne jpfinne commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

fix #24686

For a type:

type: object
required:
  - name
properties:
   name:
     type: string
     nullable: true

Ensure that the following json are accepted:

json valid
{ "name": "john" } yes
{ "name": null } yes
{ } no

Keep jspecify @Nullable annotation for builder with an improved RemoveAnnotationLambda. Other annotations like @Valid are still removed.
Ensure setter, chain setters, builder, constructors contain the correct JSpecify @Nullable annotation.

Add more combination of readonly, nullable and required in the samples.

For nullable attributes:

Update @Schema annotation to add nullable=true

For nullable container attributes + openapiNullable

Fix a potential NullPointerException in add and put item. For example new Foo()._list(null).addListItem("item")

For required+nullable attributes:

java

  • correct missing JSpecify, javax or jakarta @Nullable annotation

Spring

  • openapiNullable=false: remove @NotNull. The bean validation has no way to find out if the value is absent.
  • openapiNullable=true: remove @NotNull. This is a not intuitive. It should be possible to distinguish between null and absent. This is not the case of the Spring bean validation. It uses JsonNullableValueExtractor or JsonNullableJakartaValueExtractor to extract the value. So @NotNull fails for null and for absent.

Workaround: use an annotation @Present with a validator like the following. (it might be a nice addition to the jackson-databind-nullable project)

public class PresentValidator implements ConstraintValidator<Present, JsonNullable<?>> {
    @Override
    public boolean isValid(JsonNullable<?> value, ConstraintValidatorContext context) {
        return value != null && value.isPresent();
    }
}

PR checklist

  • Read the contribution guidelines.
  • Run the following to build the project and update samples:
    ./mvnw clean package || exit
    ./bin/generate-samples.sh ./bin/configs/*.yaml || exit
    ./bin/utils/export_docs_generators.sh || exit
    
    (For Windows users, please run the script in WSL)
    Commit all changed files.
    This is important, as CI jobs will verify all generator outputs of your HEAD commit as it would merge with master.
    These must match the expectations made by your contribution.
    You may regenerate an individual generator by passing the relevant config(s) as an argument to the script, for example ./bin/generate-samples.sh bin/configs/java*.
    IMPORTANT: Do NOT purge/delete any folders/files (e.g. tests) when regenerating the samples as manually written tests may be removed.
  • If your PR is targeting a particular programming language, @mention the technical committee members, so they are more likely to review the pull request.

Summary by cubic

Aligns Java and Spring generators with jspecify for required+nullable fields and corrects Bean Validation. Previously Spring emitted @NotNull for required+nullable; now generators annotate nullability consistently and stop emitting @NotNull for these cases, and models include @Schema(nullable = true).

  • Java/Spring templates: add constructor and chain-setter partials for nullable args; standardize nullable_var_annotations.mustache; expose removeAnnotations lambda; compute the nullable annotation from javaxPackage so javax.annotation.Nullable is not emitted under jspecify.
  • Spring Bean Validation: move @NotNull to notNull.mustache; emit only when “required AND not readOnly AND not nullable”. With openApiNullable=true, required+nullable JsonNullable<T> fields still initialize to null.
  • OpenAPI/annotations: generate @Schema(..., nullable = true) across Java library templates.
  • Samples/tests: add RequiredAndNullable schema and endpoints; assert constructor/chain-setter @Nullable and absence of javax.annotation.Nullable under jspecify; change FileApi to return FileContent with Accept: application/json.

Migration

  • Spring generators no longer emit @NotNull for required+nullable, including with openApiNullable. If you relied on Bean Validation to reject nulls, add explicit constraints or mark fields non-nullable.
  • When openApiNullable is enabled, required+nullable JsonNullable<T> fields default to null. If you depended on undefined(), update handling accordingly.

Written for commit b3f6f5e. Summary will update on new commits.

Review in cubic

@jpfinne jpfinne changed the title Bug/nullable required [JAVA] [Spring] Fix nullable field + required/readonly Aug 15, 2026
@jpfinne
jpfinne marked this pull request as ready for review August 15, 2026 12:34

@cubic-dev-ai cubic-dev-ai Bot 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.

6 issues found across 125 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="samples/client/petstore/java/native-jackson3-jspecify/src/main/java/org/openapitools/client/api/FileApi.java">

<violation number="1" location="samples/client/petstore/java/native-jackson3-jspecify/src/main/java/org/openapitools/client/api/FileApi.java:234">
P2: When the JVM default charset is not UTF-8, this response parser corrupts non-ASCII `FileContent` values before Jackson deserializes them. Decode JSON with `StandardCharsets.UTF_8` explicitly.</violation>
</file>

<file name="modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java">

<violation number="1" location="modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java:7186">
P2: These jspecify tests verify that a required non-nullable property (`getRequiredDt`) still gets `@NotNull`, but they never assert that the required+nullable properties (`getStr`/`setStr`) in the new `RequiredAndNullable` model do NOT get `@NotNull` — which is the core behavior this PR changes. The `fileContains` signatures such as `"@Nullable String getStr()"` are substring matches and still pass even if `@NotNull` were emitted as `@NotNull @Nullable String getStr()`, so they give false confidence. Add an explicit negative assertion, e.g. `.assertMethod("getStr").assertMethodAnnotations().doesNotContainWithName("NotNull")` (and likewise for `setStr`), so a regression that re-adds `@NotNull` on required+nullable members fails these tests.</violation>
</file>

<file name="samples/client/petstore/java/restclient-springBoot4-jackson3-jspecify-openapiNullable/src/main/java/org/openapitools/client/model/FileContent.java">

<violation number="1" location="samples/client/petstore/java/restclient-springBoot4-jackson3-jspecify-openapiNullable/src/main/java/org/openapitools/client/model/FileContent.java:92">
P2: The @JsonCreator constructor parameters for the nullable fields `size` and `virusScan` are missing the `@Nullable` annotation, even though the same fields are emitted as `@Nullable` on their getters and on the builder chain-setters (`size(@Nullable Integer size)`, `virusScan(@Nullable VirusScanEnum virusScan)`). The `@JsonCreator` constructor comes from the readOnly-constructor branch of `pojo.mustache` (restclient, lines ~115-124), which still renders `{{{datatypeWithEnum}}}` without the `{{>nullableArgumentWithEnum}}` helper applied to the all-args constructor at line 133. So for this all-readOnly model, every nullable constructor arg lacks `@Nullable`. This contradicts the PR's stated goal (emit jspecify `@Nullable` on nullable constructor args) and is inconsistent with `RequiredAndNullable.java` in the same sample, whose all-args constructor carries `@Nullable`. Update the readOnly constructor rendering to use the nullable args helper too.</violation>
</file>

<file name="modules/openapi-generator/src/main/resources/Java/nullableArgument_builder.mustache">

<violation number="1" location="modules/openapi-generator/src/main/resources/Java/nullableArgument_builder.mustache:1">
P2: With `useJspecify`, this wrapper removes the `@Nullable` emitted for nullable builder arguments, making them unannotated non-null types. Strip annotations only from raw `datatypeWithEnum`, while rendering `nullable_var_annotations` outside `removeAnnotations`.</violation>
</file>

<file name="samples/client/petstore/java/native-jackson3-jspecify/src/main/java/org/openapitools/client/api/RequiredAndNullableApi.java">

<violation number="1" location="samples/client/petstore/java/native-jackson3-jspecify/src/main/java/org/openapitools/client/api/RequiredAndNullableApi.java:155">
P2: When a server controls `Content-Disposition`, `prepareDownloadFile` can write outside its temporary directory because it resolves the raw filename. Reduce the filename to a basename before resolving it.</violation>

<violation number="2" location="samples/client/petstore/java/native-jackson3-jspecify/src/main/java/org/openapitools/client/api/RequiredAndNullableApi.java:234">
P2: On a JVM whose default charset is not UTF-8, non-ASCII JSON response data is decoded incorrectly before Jackson deserializes it. Decode the response body as UTF-8 explicitly.</violation>
</file>

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

Re-trigger cubic




String responseBody = new String(localVarResponseBody.readAllBytes());

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.

P2: When the JVM default charset is not UTF-8, this response parser corrupts non-ASCII FileContent values before Jackson deserializes them. Decode JSON with StandardCharsets.UTF_8 explicitly.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At samples/client/petstore/java/native-jackson3-jspecify/src/main/java/org/openapitools/client/api/FileApi.java, line 234:

<comment>When the JVM default charset is not UTF-8, this response parser corrupts non-ASCII `FileContent` values before Jackson deserializes them. Decode JSON with `StandardCharsets.UTF_8` explicitly.</comment>

<file context>
@@ -217,20 +221,31 @@ public ApiResponse<Void> fileIdGetWithHttpInfo(String id, Map<String, String> he
+
+        
+        
+        String responseBody = new String(localVarResponseBody.readAllBytes());
+        FileContent responseValue = responseBody.isBlank()? null: memberVarObjectMapper.readValue(responseBody, new TypeReference<FileContent>() {});
+        
</file context>
Suggested change
String responseBody = new String(localVarResponseBody.readAllBytes());
String responseBody = new String(localVarResponseBody.readAllBytes(), java.nio.charset.StandardCharsets.UTF_8);




String responseBody = new String(localVarResponseBody.readAllBytes());

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.

P2: On a JVM whose default charset is not UTF-8, non-ASCII JSON response data is decoded incorrectly before Jackson deserializes it. Decode the response body as UTF-8 explicitly.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At samples/client/petstore/java/native-jackson3-jspecify/src/main/java/org/openapitools/client/api/RequiredAndNullableApi.java, line 234:

<comment>On a JVM whose default charset is not UTF-8, non-ASCII JSON response data is decoded incorrectly before Jackson deserializes it. Decode the response body as UTF-8 explicitly.</comment>

<file context>
@@ -0,0 +1,289 @@
+
+        
+        
+        String responseBody = new String(localVarResponseBody.readAllBytes());
+        RequiredAndNullable responseValue = responseBody.isBlank()? null: memberVarObjectMapper.readValue(responseBody, new TypeReference<RequiredAndNullable>() {});
+        
</file context>
Suggested change
String responseBody = new String(localVarResponseBody.readAllBytes());
String responseBody = new String(localVarResponseBody.readAllBytes(), java.nio.charset.StandardCharsets.UTF_8);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Already existed

File file = null;
if (filename != null) {
java.nio.file.Path tempDir = java.nio.file.Files.createTempDirectory("swagger-gen-native");
java.nio.file.Path filePath = java.nio.file.Files.createFile(tempDir.resolve(filename));

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.

P2: When a server controls Content-Disposition, prepareDownloadFile can write outside its temporary directory because it resolves the raw filename. Reduce the filename to a basename before resolving it.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At samples/client/petstore/java/native-jackson3-jspecify/src/main/java/org/openapitools/client/api/RequiredAndNullableApi.java, line 155:

<comment>When a server controls `Content-Disposition`, `prepareDownloadFile` can write outside its temporary directory because it resolves the raw filename. Reduce the filename to a basename before resolving it.</comment>

<file context>
@@ -0,0 +1,289 @@
+    File file = null;
+    if (filename != null) {
+      java.nio.file.Path tempDir = java.nio.file.Files.createTempDirectory("swagger-gen-native");
+      java.nio.file.Path filePath = java.nio.file.Files.createFile(tempDir.resolve(filename));
+      file = filePath.toFile();
+      tempDir.toFile().deleteOnExit();   // best effort cleanup
</file context>
Suggested change
java.nio.file.Path filePath = java.nio.file.Files.createFile(tempDir.resolve(filename));
java.nio.file.Path filePath = java.nio.file.Files.createFile(tempDir.resolve(java.nio.file.Path.of(filename).getFileName()));

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Already existed

).fileDoesNotContain(
"javax.annotation.Nullable",
"jakarta.annotation.Nullable")
.assertMethod("getRequiredDt").assertMethodAnnotations().containsWithName("NotNull").containsWithName("Valid");

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.

P2: These jspecify tests verify that a required non-nullable property (getRequiredDt) still gets @NotNull, but they never assert that the required+nullable properties (getStr/setStr) in the new RequiredAndNullable model do NOT get @NotNull — which is the core behavior this PR changes. The fileContains signatures such as "@Nullable String getStr()" are substring matches and still pass even if @NotNull were emitted as @NotNull @Nullable String getStr(), so they give false confidence. Add an explicit negative assertion, e.g. .assertMethod("getStr").assertMethodAnnotations().doesNotContainWithName("NotNull") (and likewise for setStr), so a regression that re-adds @NotNull on required+nullable members fails these tests.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java, line 7186:

<comment>These jspecify tests verify that a required non-nullable property (`getRequiredDt`) still gets `@NotNull`, but they never assert that the required+nullable properties (`getStr`/`setStr`) in the new `RequiredAndNullable` model do NOT get `@NotNull` — which is the core behavior this PR changes. The `fileContains` signatures such as `"@Nullable String getStr()"` are substring matches and still pass even if `@NotNull` were emitted as `@NotNull @Nullable String getStr()`, so they give false confidence. Add an explicit negative assertion, e.g. `.assertMethod("getStr").assertMethodAnnotations().doesNotContainWithName("NotNull")` (and likewise for `setStr`), so a regression that re-adds `@NotNull` on required+nullable members fails these tests.</comment>

<file context>
@@ -7179,7 +7180,10 @@ public void testJspecify(String library, int springBootVersion) throws IOExcepti
+                ).fileDoesNotContain(
+                        "javax.annotation.Nullable",
+                        "jakarta.annotation.Nullable")
+                .assertMethod("getRequiredDt").assertMethodAnnotations().containsWithName("NotNull").containsWithName("Valid");
         JavaFileAssert.assertThat(files.get("FooApi.java"))
                 .assertTypeAnnotations().doesImportAnnotation("org.jspecify.annotations.Nullable").toType()
</file context>

Comment thread modules/openapi-generator/src/test/resources/3_0/java/jspecify.yaml Outdated
@jpfinne jpfinne changed the title [JAVA] [Spring] Fix nullable field + required/readonly [JAVA] [Spring] JSpecify, fix nullable field + required Aug 15, 2026
@jpfinne jpfinne changed the title [JAVA] [Spring] JSpecify, fix nullable field + required [JAVA] [Spring] JSpecify, fix nullable + required field Aug 15, 2026

@cubic-dev-ai cubic-dev-ai Bot 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.

All reported issues were addressed across 27 files (changes from recent commits).

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

Re-trigger cubic

@jpfinne
jpfinne marked this pull request as draft August 16, 2026 05:26
Make JSonNullable<> field = null for nullable+required
@jpfinne
jpfinne marked this pull request as ready for review August 16, 2026 08:27

@cubic-dev-ai cubic-dev-ai Bot 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.

All reported issues were addressed across 197 files

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

Re-trigger cubic

@jpfinne
jpfinne marked this pull request as draft August 16, 2026 08:42
@jpfinne
jpfinne marked this pull request as ready for review August 16, 2026 14:56

@cubic-dev-ai cubic-dev-ai Bot 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.

No issues found across 233 files

Re-trigger cubic

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.

[BUG] [Java] [spring] Properties that are both required and nullable are marked NotNull

1 participant