[JAVA] [Spring] JSpecify, fix nullable + required field - #24711
Conversation
merge master
# Conflicts: # modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java
There was a problem hiding this comment.
7 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="samples/client/petstore/java/webclient-springBoot4-jackson3-jspecify/README.md">
<violation number="1" location="samples/client/petstore/java/webclient-springBoot4-jackson3-jspecify/README.md:98">
P3: The new README example `FileContent result = apiInstance.fileIdGet(id);` does not compile: this is a reactive WebClient client and `FileApi#fileIdGet` returns `Mono<FileContent>`, not `FileContent`. Assigning the `Mono` to a `FileContent` variable is a type mismatch, and the reactive result is never subscribed. Use `Mono<FileContent>` plus `.block()` (or subscribe) in the example, consistent with the actual returned type.</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()); |
There was a problem hiding this comment.
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>
| String responseBody = new String(localVarResponseBody.readAllBytes()); | |
| String responseBody = new String(localVarResponseBody.readAllBytes(), java.nio.charset.StandardCharsets.UTF_8); |
|
|
||
|
|
||
|
|
||
| String responseBody = new String(localVarResponseBody.readAllBytes()); |
There was a problem hiding this comment.
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>
| String responseBody = new String(localVarResponseBody.readAllBytes()); | |
| String responseBody = new String(localVarResponseBody.readAllBytes(), java.nio.charset.StandardCharsets.UTF_8); |
| 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)); |
There was a problem hiding this comment.
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>
| 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())); |
| ).fileDoesNotContain( | ||
| "javax.annotation.Nullable", | ||
| "jakarta.annotation.Nullable") | ||
| .assertMethod("getRequiredDt").assertMethodAnnotations().containsWithName("NotNull").containsWithName("Valid"); |
There was a problem hiding this comment.
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>
| String id = "id_example"; // String | | ||
| try { | ||
| apiInstance.fileIdGet(id); | ||
| FileContent result = apiInstance.fileIdGet(id); |
There was a problem hiding this comment.
P3: The new README example FileContent result = apiInstance.fileIdGet(id); does not compile: this is a reactive WebClient client and FileApi#fileIdGet returns Mono<FileContent>, not FileContent. Assigning the Mono to a FileContent variable is a type mismatch, and the reactive result is never subscribed. Use Mono<FileContent> plus .block() (or subscribe) in the example, consistent with the actual returned type.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At samples/client/petstore/java/webclient-springBoot4-jackson3-jspecify/README.md, line 98:
<comment>The new README example `FileContent result = apiInstance.fileIdGet(id);` does not compile: this is a reactive WebClient client and `FileApi#fileIdGet` returns `Mono<FileContent>`, not `FileContent`. Assigning the `Mono` to a `FileContent` variable is a type mismatch, and the reactive result is never subscribed. Use `Mono<FileContent>` plus `.block()` (or subscribe) in the example, consistent with the actual returned type.</comment>
<file context>
@@ -95,7 +95,8 @@ public class FileApiExample {
String id = "id_example"; // String |
try {
- apiInstance.fileIdGet(id);
+ FileContent result = apiInstance.fileIdGet(id);
+ System.out.println(result);
} catch (ApiException e) {
</file context>
There was a problem hiding this comment.
All reported issues were addressed across 27 files (changes from recent commits).
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
Make JSonNullable<> field = null for nullable+required
There was a problem hiding this comment.
3 issues found across 197 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/resttemplate-springBoot4-jackson3-jspecify/src/main/java/org/openapitools/client/model/RequiredAndNullable.java">
<violation number="1" location="samples/client/petstore/java/resttemplate-springBoot4-jackson3-jspecify/src/main/java/org/openapitools/client/model/RequiredAndNullable.java:17">
P3: `java.util.Arrays` is imported twice (and is unused in the file body). The template's generic import block and the per-array-var import block each emit `import java.util.Arrays;`, producing lines 17 and 24. Remove the redundant duplicate import in the generated model (the root cause is the template emitting the same import from two sources).</violation>
</file>
<file name="samples/openapi3/server/petstore/springboot-4-jspecify-useOptional/src/main/java/org/openapitools/model/RequiredAndNullable.java">
<violation number="1" location="samples/openapi3/server/petstore/springboot-4-jspecify-useOptional/src/main/java/org/openapitools/model/RequiredAndNullable.java:161">
P2: In `addListItem`, when `_list` is a present-but-null `JsonNullable` (i.e. created via `JsonNullable.of(null)`), the guard passes but the add throws an NPE. This is reachable: the public constructor takes `@Nullable List<String> _list` and wraps it with `JsonNullable.of(_list)` (line 57), so `new RequiredAndNullable(..., null)` yields `_list = JsonNullable.of(null)`; then `!this._list.isPresent()` is false, `this._list.get()` returns null, and `.add(_listItem)` throws. Guard on whether the wrapped value is actually a non-null list before calling `.get().add(...)`.</violation>
</file>
<file name="modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java">
<violation number="1" location="modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java:4720">
P3: testJspecify uses the bare string key `"annotationLibrary"` while testJspecify_openapiNullable (added in this same PR) uses the `ANNOTATION_LIBRARY` constant for the identical setting. Import and use the `ANNOTATION_LIBRARY` constant from `DocumentationProviderFeatures` to keep the two tests consistent and avoid typos in the magic string.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| public RequiredAndNullable addListItem(String _listItem) { | ||
| if (this._list == null || !this._list.isPresent()) { | ||
| this._list = JsonNullable.of(new ArrayList<>()); | ||
| } | ||
| this._list.get().add(_listItem); | ||
| return this; |
There was a problem hiding this comment.
P2: In addListItem, when _list is a present-but-null JsonNullable (i.e. created via JsonNullable.of(null)), the guard passes but the add throws an NPE. This is reachable: the public constructor takes @Nullable List<String> _list and wraps it with JsonNullable.of(_list) (line 57), so new RequiredAndNullable(..., null) yields _list = JsonNullable.of(null); then !this._list.isPresent() is false, this._list.get() returns null, and .add(_listItem) throws. Guard on whether the wrapped value is actually a non-null list before calling .get().add(...).
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At samples/openapi3/server/petstore/springboot-4-jspecify-useOptional/src/main/java/org/openapitools/model/RequiredAndNullable.java, line 161:
<comment>In `addListItem`, when `_list` is a present-but-null `JsonNullable` (i.e. created via `JsonNullable.of(null)`), the guard passes but the add throws an NPE. This is reachable: the public constructor takes `@Nullable List<String> _list` and wraps it with `JsonNullable.of(_list)` (line 57), so `new RequiredAndNullable(..., null)` yields `_list = JsonNullable.of(null)`; then `!this._list.isPresent()` is false, `this._list.get()` returns null, and `.add(_listItem)` throws. Guard on whether the wrapped value is actually a non-null list before calling `.get().add(...)`.</comment>
<file context>
@@ -0,0 +1,334 @@
+ return this;
+ }
+
+ public RequiredAndNullable addListItem(String _listItem) {
+ if (this._list == null || !this._list.isPresent()) {
+ this._list = JsonNullable.of(new ArrayList<>());
</file context>
| public RequiredAndNullable addListItem(String _listItem) { | |
| if (this._list == null || !this._list.isPresent()) { | |
| this._list = JsonNullable.of(new ArrayList<>()); | |
| } | |
| this._list.get().add(_listItem); | |
| return this; | |
| public RequiredAndNullable addListItem(String _listItem) { | |
| if (this._list == null || !this._list.isPresent() || this._list.get() == null) { | |
| this._list = JsonNullable.of(new ArrayList<>()); | |
| } | |
| this._list.get().add(_listItem); | |
| return this; | |
| } |
| package org.openapitools.client.model; | ||
|
|
||
| import java.util.Objects; | ||
| import java.util.Arrays; |
There was a problem hiding this comment.
P3: java.util.Arrays is imported twice (and is unused in the file body). The template's generic import block and the per-array-var import block each emit import java.util.Arrays;, producing lines 17 and 24. Remove the redundant duplicate import in the generated model (the root cause is the template emitting the same import from two sources).
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At samples/client/petstore/java/resttemplate-springBoot4-jackson3-jspecify/src/main/java/org/openapitools/client/model/RequiredAndNullable.java, line 17:
<comment>`java.util.Arrays` is imported twice (and is unused in the file body). The template's generic import block and the per-array-var import block each emit `import java.util.Arrays;`, producing lines 17 and 24. Remove the redundant duplicate import in the generated model (the root cause is the template emitting the same import from two sources).</comment>
<file context>
@@ -0,0 +1,328 @@
+package org.openapitools.client.model;
+
+import java.util.Objects;
+import java.util.Arrays;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
</file context>
| USE_ABSTRACTION_FOR_FILES, true | ||
| ), | ||
| USE_ABSTRACTION_FOR_FILES, true, | ||
| "annotationLibrary", "swagger2" |
There was a problem hiding this comment.
P3: testJspecify uses the bare string key "annotationLibrary" while testJspecify_openapiNullable (added in this same PR) uses the ANNOTATION_LIBRARY constant for the identical setting. Import and use the ANNOTATION_LIBRARY constant from DocumentationProviderFeatures to keep the two tests consistent and avoid typos in the magic string.
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/JavaClientCodegenTest.java, line 4720:
<comment>testJspecify uses the bare string key `"annotationLibrary"` while testJspecify_openapiNullable (added in this same PR) uses the `ANNOTATION_LIBRARY` constant for the identical setting. Import and use the `ANNOTATION_LIBRARY` constant from `DocumentationProviderFeatures` to keep the two tests consistent and avoid typos in the magic string.</comment>
<file context>
@@ -4697,27 +4697,28 @@ public void testRestClientMultipartFormParamsGuardAgainstEmptyLists() {
- USE_ABSTRACTION_FOR_FILES, true
- ),
+ USE_ABSTRACTION_FOR_FILES, true,
+ "annotationLibrary", "swagger2"
+ ),
codegenConfigurator ->
</file context>
fix #24686
For a type:
Ensure that the following json are accepted:
Keep jspecify
@Nullableannotation for builder with an improved RemoveAnnotationLambda. Other annotations like@Validare still removed.Ensure setter, chain setters, builder, constructors contain the correct JSpecify
@Nullableannotation.Add more combination of readonly, nullable and required in the samples.
For nullable attributes:
Update
@Schemaannotation to addnullable=trueFor required+nullable attributes:
For Spring:
@NotNullJsonNullable<>field,. so that bean validation uses@NotNullon getter to detect missing attributes (like{ })For java:
@NullableannotationPR checklist
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.
Summary by cubic
Fixes Java and Spring handling of required+nullable under
jspecifyand aligns Bean Validation. Previously these fields were non-null and Spring added@NotNull; now required+nullable are annotated@Nullableon fields, constructors, builders, and chain setters, and@NotNullis emitted only when appropriate. Also addsnullable = truein@Schemafor nullable properties and updatesFileApito returnFileContentJSON.nullable_var_annotations.mustache; exposeremoveAnnotationslambda to templates; compute fully qualified nullable annotation fromjavaxPackagesojavax.annotation.Nullableis not emitted underjspecify.@NotNulltonotNull.mustache; emit only when “required AND not readOnly AND (not nullable ORopenApiNullable)”; keep@NotNullforJsonNullablefields. WithopenApiNullable=true, required+nullableJsonNullable<T>fields now initialize tonull(notundefined()), so validation can trigger.@Schema(..., nullable = true)for nullable properties across Java library templates.RequiredAndNullableschema and endpoints; assert constructor/chain-setter annotations and absence ofjavax.annotation.Nullableunderjspecify; changeFileApito returnFileContentwithAccept: application/json.Migration
@NotNullfor required+nullable unless you enableopenApiNullable. If you relied on Bean Validation to reject nulls withoutopenApiNullable, add explicit constraints or mark fields non-nullable.openApiNullableis enabled, required+nullableJsonNullable<T>fields now default tonull. If you depended onundefined(), update handling accordingly.Written for commit f11445b. Summary will update on new commits.