From 9499094e31ad64e8fc30080c2e5c0d7a848c2195 Mon Sep 17 00:00:00 2001 From: TheNevim Date: Fri, 20 Mar 2026 06:43:14 +0100 Subject: [PATCH 1/2] fix(jaxrs): correct add/remove methods for JsonNullable> fields #23251 Signed-off-by: TheNevim --- .../resources/JavaJaxRS/spec/pojo.mustache | 50 ++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/modules/openapi-generator/src/main/resources/JavaJaxRS/spec/pojo.mustache b/modules/openapi-generator/src/main/resources/JavaJaxRS/spec/pojo.mustache index 83970c86ca74..dccf69cdf4ba 100644 --- a/modules/openapi-generator/src/main/resources/JavaJaxRS/spec/pojo.mustache +++ b/modules/openapi-generator/src/main/resources/JavaJaxRS/spec/pojo.mustache @@ -173,36 +173,84 @@ public class {{classname}} {{#parent}}extends {{{.}}}{{/parent}} {{#vendorExtens {{#isArray}} public {{classname}} add{{nameInPascalCase}}Item({{{items.datatypeWithEnum}}} {{name}}Item) { + {{#vendorExtensions.x-is-jackson-optional-nullable}} + if (this.{{name}} == null || !this.{{name}}.isPresent() || this.{{name}}.get() == null) { + this.{{name}} = JsonNullable.<{{{datatypeWithEnum}}}>of({{{defaultValue}}}{{^defaultValue}}new {{#uniqueItems}}LinkedHashSet{{/uniqueItems}}{{^uniqueItems}}ArrayList{{/uniqueItems}}<>(){{/defaultValue}}); + } + try { + this.{{name}}.get().add({{name}}Item); + } catch (java.util.NoSuchElementException e) { + // this can never happen, as we make sure above that the value is present + } + return this; + {{/vendorExtensions.x-is-jackson-optional-nullable}} + {{^vendorExtensions.x-is-jackson-optional-nullable}} if (this.{{name}} == null) { this.{{name}} = {{{defaultValue}}}{{^defaultValue}}new {{#uniqueItems}}LinkedHashSet{{/uniqueItems}}{{^uniqueItems}}ArrayList{{/uniqueItems}}<>(){{/defaultValue}}; } this.{{name}}.add({{name}}Item); return this; + {{/vendorExtensions.x-is-jackson-optional-nullable}} } public {{classname}} remove{{nameInPascalCase}}Item({{{items.datatypeWithEnum}}} {{name}}Item) { + {{#vendorExtensions.x-is-jackson-optional-nullable}} + if ({{name}}Item != null && this.{{name}} != null && this.{{name}}.isPresent()) { + try { + this.{{name}}.get().remove({{name}}Item); + } catch (java.util.NoSuchElementException e) { + // this can never happen, as we make sure above that the value is present + } + } + {{/vendorExtensions.x-is-jackson-optional-nullable}} + {{^vendorExtensions.x-is-jackson-optional-nullable}} if ({{name}}Item != null && this.{{name}} != null) { this.{{name}}.remove({{name}}Item); } + {{/vendorExtensions.x-is-jackson-optional-nullable}} return this; } {{/isArray}} {{#isMap}} public {{classname}} put{{nameInPascalCase}}Item(String key, {{{items.datatypeWithEnum}}} {{name}}Item) { + {{#vendorExtensions.x-is-jackson-optional-nullable}} + if (this.{{name}} == null || !this.{{name}}.isPresent() || this.{{name}}.get() == null) { + this.{{name}} = JsonNullable.<{{{datatypeWithEnum}}}>of({{{defaultValue}}}{{^defaultValue}}new HashMap<>(){{/defaultValue}}); + } + try { + this.{{name}}.get().put(key, {{name}}Item); + } catch (java.util.NoSuchElementException e) { + // this can never happen, as we make sure above that the value is present + } + return this; + {{/vendorExtensions.x-is-jackson-optional-nullable}} + {{^vendorExtensions.x-is-jackson-optional-nullable}} if (this.{{name}} == null) { this.{{name}} = {{{defaultValue}}}{{^defaultValue}}new HashMap<>(){{/defaultValue}}; } this.{{name}}.put(key, {{name}}Item); return this; + {{/vendorExtensions.x-is-jackson-optional-nullable}} } public {{classname}} remove{{nameInPascalCase}}Item(String key) { + {{#vendorExtensions.x-is-jackson-optional-nullable}} + if (this.{{name}} != null && this.{{name}}.isPresent()) { + try { + this.{{name}}.get().remove(key); + } catch (java.util.NoSuchElementException e) { + // this can never happen, as we make sure above that the value is present + } + } + {{/vendorExtensions.x-is-jackson-optional-nullable}} + {{^vendorExtensions.x-is-jackson-optional-nullable}} if (this.{{name}} != null) { this.{{name}}.remove(key); } + {{/vendorExtensions.x-is-jackson-optional-nullable}} return this; } @@ -286,4 +334,4 @@ public class {{classname}} {{#parent}}extends {{{.}}}{{/parent}} {{#vendorExtens } {{/vars}} }{{/additionalProperties}}{{/generateBuilders}} -} +} \ No newline at end of file From ad360127865116efbb8141208993933aed748223 Mon Sep 17 00:00:00 2001 From: TheNevim Date: Sun, 22 Mar 2026 10:48:46 +0100 Subject: [PATCH 2/2] fix(jaxrs): add test for generation of add/remove methods for JsonNullable> fields #23251 Signed-off-by: TheNevim --- .../jaxrs/JavaJAXRSSpecServerCodegenTest.java | 38 ++++++++++++++++ .../src/test/resources/bugs/issue_23251.yaml | 44 +++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 modules/openapi-generator/src/test/resources/bugs/issue_23251.yaml diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/jaxrs/JavaJAXRSSpecServerCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/jaxrs/JavaJAXRSSpecServerCodegenTest.java index 2e9e2e2153d2..e480e362025c 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/jaxrs/JavaJAXRSSpecServerCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/jaxrs/JavaJAXRSSpecServerCodegenTest.java @@ -1236,5 +1236,43 @@ public void disableGenerateJsonCreator() throws Exception { assertFileNotContains(files.get("RequiredProperties.java").toPath(), "@JsonCreator"); } + + @Test + public void testGenerateJsonNullableListFieldsHelperMethodReferences_issue23251() throws Exception { + Map properties = new HashMap<>(); + properties.put(OPENAPI_NULLABLE, "true"); + + File output = Files.createTempDirectory("test").toFile(); + + final CodegenConfigurator configurator = new CodegenConfigurator() + .setGeneratorName("jaxrs-spec") + .setAdditionalProperties(properties) + .setInputSpec("src/test/resources/bugs/issue_23251.yaml") + .setOutputDir(output.getAbsolutePath().replace("\\", "/")); + + final ClientOptInput clientOptInput = configurator.toClientOptInput(); + DefaultGenerator generator = new DefaultGenerator(); + List files = generator.opts(clientOptInput).generate(); + + validateJavaSourceFiles(files); + + TestUtils.ensureContainsFile(files, output, "src/gen/java/org/openapitools/model/BugResponse.java"); + + // Assert that the generated model contains JsonNullable fields + assertFileContains(output.toPath().resolve("src/gen/java/org/openapitools/model/BugResponse.java"), + "private JsonNullable nullableField = JsonNullable.undefined();", + "private JsonNullable> nullableList = JsonNullable.>undefined();", + "private JsonNullable> nullableObjectList = JsonNullable.>undefined();" + ); + + // Assert that the generated model contains correct add and remove helper methods reference for JsonNullable fields + assertFileContains(output.toPath().resolve("src/gen/java/org/openapitools/model/BugResponse.java"), + "this.nullableList.get().add(nullableListItem);", + "this.nullableList.get().remove(nullableListItem);", + "this.nullableObjectList.get().add(nullableObjectListItem);", + "this.nullableObjectList.get().remove(nullableObjectListItem);"); + + output.deleteOnExit(); + } } diff --git a/modules/openapi-generator/src/test/resources/bugs/issue_23251.yaml b/modules/openapi-generator/src/test/resources/bugs/issue_23251.yaml new file mode 100644 index 000000000000..1529399dd38f --- /dev/null +++ b/modules/openapi-generator/src/test/resources/bugs/issue_23251.yaml @@ -0,0 +1,44 @@ +openapi: 3.0.2 +info: + title: Reproduce + version: 1.0.0 +paths: + /test: + get: + summary: test + operationId: test + responses: + 200: + description: Test + content: + application/json: + schema: + $ref: '#/components/schemas/BugResponse' +components: + schemas: + BugResponse: + type: object + properties: + nullableField: + type: string + nullable: true + x-is-jackson-optional-nullable: true + nullableList: + type: array + items: + type: string + nullable: true + x-is-jackson-optional-nullable: true + nullableObjectList: + type: array + items: + $ref: '#/components/schemas/NestedResponse' + nullable: true + x-is-jackson-optional-nullable: true + NestedResponse: + type: object + properties: + nestedField: + type: string + nullable: true + x-is-jackson-optional-nullable: true \ No newline at end of file