diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractJavaCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractJavaCodegen.java index 3102703b8339..509da5be0597 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractJavaCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractJavaCodegen.java @@ -2053,6 +2053,22 @@ public abstract class AbstractJavaCodegen extends DefaultCodegen implements Code } } + /** + * Search for property by {@link CodegenProperty#name} + * @param name - name to search for + * @param properties - list of properties + * @return either found property or {@link Optional#empty()} if nothing has been found + */ + protected Optional findByName(String name, List properties) { + if (properties == null || properties.isEmpty()) { + return Optional.empty(); + } + + return properties.stream() + .filter(p -> p.name.equals(name)) + .findFirst(); + } + /** * This method removes all implicit header parameters from the list of parameters * diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaCXFClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaCXFClientCodegen.java index a45e743b8f98..e83224d99956 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaCXFClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaCXFClientCodegen.java @@ -159,6 +159,8 @@ public class JavaCXFClientCodegen extends AbstractJavaCodegen if (openApiNullable) { if (Boolean.FALSE.equals(property.required) && Boolean.TRUE.equals(property.isNullable)) { property.getVendorExtensions().put("x-is-jackson-optional-nullable", true); + findByName(property.name, model.readOnlyVars) + .ifPresent(p -> p.getVendorExtensions().put("x-is-jackson-optional-nullable", true)); model.imports.add("JsonNullable"); model.imports.add("JsonIgnore"); } diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaClientCodegen.java index fbbcc6c3fec5..cedd19cfafb4 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaClientCodegen.java @@ -926,6 +926,8 @@ public class JavaClientCodegen extends AbstractJavaCodegen // only add JsonNullable and related imports to optional and nullable values addImports |= isOptionalNullable; var.getVendorExtensions().put("x-is-jackson-optional-nullable", isOptionalNullable); + findByName(var.name, cm.readOnlyVars) + .ifPresent(p -> p.getVendorExtensions().put("x-is-jackson-optional-nullable", isOptionalNullable)); } if (Boolean.TRUE.equals(var.getVendorExtensions().get("x-enum-as-string"))) { diff --git a/modules/openapi-generator/src/main/resources/Java/libraries/jersey2/pojo.mustache b/modules/openapi-generator/src/main/resources/Java/libraries/jersey2/pojo.mustache index c138284ed484..9faeed2eaf3d 100644 --- a/modules/openapi-generator/src/main/resources/Java/libraries/jersey2/pojo.mustache +++ b/modules/openapi-generator/src/main/resources/Java/libraries/jersey2/pojo.mustache @@ -100,7 +100,7 @@ public class {{classname}} {{#parent}}extends {{{.}}} {{/parent}}{{#vendorExtens ) { this(); {{#readOnlyVars}} - this.{{name}} = {{name}}; + this.{{name}} = {{#vendorExtensions.x-is-jackson-optional-nullable}}{{name}} == null ? JsonNullable.<{{{datatypeWithEnum}}}>undefined() : JsonNullable.of({{name}}){{/vendorExtensions.x-is-jackson-optional-nullable}}{{^vendorExtensions.x-is-jackson-optional-nullable}}{{name}}{{/vendorExtensions.x-is-jackson-optional-nullable}}; {{/readOnlyVars}} }{{/jackson}}{{/withXml}}{{/vendorExtensions.x-has-readonly-properties}} {{#vars}} diff --git a/modules/openapi-generator/src/main/resources/Java/libraries/native/pojo.mustache b/modules/openapi-generator/src/main/resources/Java/libraries/native/pojo.mustache index 776532461da7..804a2154e0e8 100644 --- a/modules/openapi-generator/src/main/resources/Java/libraries/native/pojo.mustache +++ b/modules/openapi-generator/src/main/resources/Java/libraries/native/pojo.mustache @@ -100,7 +100,7 @@ public class {{classname}} {{#parent}}extends {{{.}}} {{/parent}}{{#vendorExtens ) { this(); {{#readOnlyVars}} - this.{{name}} = {{name}}; + this.{{name}} = {{#vendorExtensions.x-is-jackson-optional-nullable}}{{name}} == null ? JsonNullable.<{{{datatypeWithEnum}}}>undefined() : JsonNullable.of({{name}}){{/vendorExtensions.x-is-jackson-optional-nullable}}{{^vendorExtensions.x-is-jackson-optional-nullable}}{{name}}{{/vendorExtensions.x-is-jackson-optional-nullable}}; {{/readOnlyVars}} }{{/withXml}}{{/vendorExtensions.x-has-readonly-properties}} {{#vars}} diff --git a/modules/openapi-generator/src/main/resources/Java/pojo.mustache b/modules/openapi-generator/src/main/resources/Java/pojo.mustache index 7345b16da344..84ea11480c7a 100644 --- a/modules/openapi-generator/src/main/resources/Java/pojo.mustache +++ b/modules/openapi-generator/src/main/resources/Java/pojo.mustache @@ -93,12 +93,12 @@ public class {{classname}} {{#parent}}extends {{{.}}} {{/parent}}{{#vendorExtens {{#jsonb}}@JsonbCreator{{/jsonb}}{{#jackson}}@JsonCreator{{/jackson}} public {{classname}}( {{#readOnlyVars}} - {{#jsonb}}@JsonbProperty("{{baseName}}"){{/jsonb}}{{#jackson}}@JsonProperty(JSON_PROPERTY_{{nameInSnakeCase}}){{/jackson}} {{{datatypeWithEnum}}} {{name}}{{^-last}}, {{/-last}} + {{#jsonb}}@JsonbProperty(value = "{{baseName}}"{{^required}}, nillable = true{{/required}}){{/jsonb}}{{#jackson}}@JsonProperty(JSON_PROPERTY_{{nameInSnakeCase}}){{/jackson}} {{{datatypeWithEnum}}} {{name}}{{^-last}}, {{/-last}} {{/readOnlyVars}} ) { this(); {{#readOnlyVars}} - this.{{name}} = {{name}}; + this.{{name}} = {{#vendorExtensions.x-is-jackson-optional-nullable}}{{name}} == null ? JsonNullable.<{{{datatypeWithEnum}}}>undefined() : JsonNullable.of({{name}}){{/vendorExtensions.x-is-jackson-optional-nullable}}{{^vendorExtensions.x-is-jackson-optional-nullable}}{{name}}{{/vendorExtensions.x-is-jackson-optional-nullable}}; {{/readOnlyVars}} }{{/withXml}}{{/vendorExtensions.x-has-readonly-properties}} {{#vars}} diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java index 7b1c4215cc20..06f5aadc9f7a 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java @@ -1534,10 +1534,42 @@ public class JavaClientCodegenTest { .collect(Collectors.toMap(File::getName, Function.identity())); JavaFileAssert.assertThat(files.get("Foo.java")) + .assertConstructor("String", "Integer") + .hasParameter("b") + .assertParameterAnnotations() + .containsWithNameAndAttributes("JsonbProperty", ImmutableMap.of("value", "\"b\"", "nillable", "true")) + .toParameter().toConstructor() + .hasParameter("c") + .assertParameterAnnotations() + .containsWithNameAndAttributes("JsonbProperty", ImmutableMap.of("value", "\"c\"")); + } + + @Test + public void testWebClientJsonCreatorWithNullable_issue12790() throws Exception { + Map properties = new HashMap<>(); + properties.put(AbstractJavaCodegen.OPENAPI_NULLABLE, "true"); + + File output = Files.createTempDirectory("test").toFile(); + output.deleteOnExit(); + + final CodegenConfigurator configurator = new CodegenConfigurator() + .setAdditionalProperties(properties) + .setGeneratorName("java") + .setLibrary(JavaClientCodegen.WEBCLIENT) + .setInputSpec("src/test/resources/bugs/issue_12790.yaml") + .setOutputDir(output.getAbsolutePath().replace("\\", "/")); + + final ClientOptInput clientOptInput = configurator.toClientOptInput(); + DefaultGenerator generator = new DefaultGenerator(); + Map files = generator.opts(clientOptInput).generate().stream() + .collect(Collectors.toMap(File::getName, Function.identity())); + + JavaFileAssert.assertThat(files.get("TestObject.java")) .printFileContent() - .fileContains( - "@JsonbProperty(value = \"b\", nillable = true) String b", - "@JsonbProperty(value = \"c\") Integer c" + .assertConstructor("String", "String") + .bodyContainsLines( + "this.nullableProperty = nullableProperty == null ? JsonNullable.undefined() : JsonNullable.of(nullableProperty);", + "this.notNullableProperty = notNullableProperty;" ); } diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/assertions/ConstructorAssert.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/assertions/ConstructorAssert.java new file mode 100644 index 000000000000..c0296c5cea61 --- /dev/null +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/assertions/ConstructorAssert.java @@ -0,0 +1,97 @@ +package org.openapitools.codegen.java.assertions; + +import com.github.javaparser.ast.body.ConstructorDeclaration; +import com.github.javaparser.ast.body.Parameter; +import com.github.javaparser.ast.nodeTypes.NodeWithName; +import org.assertj.core.api.AbstractAssert; +import org.assertj.core.api.Assertions; +import org.assertj.core.util.CanIgnoreReturnValue; + +import java.util.Arrays; +import java.util.Optional; +import java.util.stream.Collectors; + +@CanIgnoreReturnValue +public class ConstructorAssert extends AbstractAssert { + + private final JavaFileAssert fileAssert; + private final String signature; + + ConstructorAssert(final JavaFileAssert fileAssert, final ConstructorDeclaration constructorDeclaration) { + super(constructorDeclaration, ConstructorAssert.class); + this.fileAssert = fileAssert; + this.signature = constructorDeclaration.getDeclarationAsString(); + } + + public JavaFileAssert toFileAssert() { + return fileAssert; + } + + public MethodAnnotationAssert assertConstructorAnnotations() { + return new MethodAnnotationAssert(this, actual.getAnnotations()); + } + + public ParameterAssert hasParameter(final String paramName) { + final Optional parameter = actual.getParameterByName(paramName); + Assertions.assertThat(parameter) + .withFailMessage("Constructor %s should have parameter %s, but it doesn't", signature, paramName) + .isPresent(); + return new ParameterAssert(this, parameter.get()); + } + + public ConstructorAssert doesNotHaveParameter(final String paramName) { + Assertions.assertThat(actual.getParameterByName(paramName)) + .withFailMessage("Constructor %s shouldn't have parameter %s, but it does", signature, paramName) + .isEmpty(); + return this; + } + + public ConstructorAssert bodyContainsLines(final String... lines) { + final String actualBody = actual.getTokenRange() + .orElseThrow(() -> new IllegalStateException("Can't get constructor body")) + .toString(); + Assertions.assertThat(actualBody) + .withFailMessage( + "Constructor's %s body should contains lines\n====\n%s\n====\nbut actually was\n====\n%s\n====", + signature, Arrays.stream(lines).collect(Collectors.joining(System.lineSeparator())), actualBody + ) + .contains(lines); + + return this; + } + + public ConstructorAssert doesNotHaveComment() { + Assertions.assertThat(actual.getJavadocComment()) + .withFailMessage("Constructor %s shouldn't contains comment, but it does", signature) + .isEmpty(); + return this; + } + + public ConstructorAssert commentContainsLines(final String... lines) { + Assertions.assertThat(actual.getJavadocComment()) + .withFailMessage("Constructor %s should contains comment, but it doesn't", signature) + .isPresent(); + final String actualComment = actual.getJavadocComment().get().getContent(); + Assertions.assertThat(actualComment) + .withFailMessage( + "Constructor's %s comment should contains lines\n====\n%s\n====\nbut actually was\n====%s\n====", + signature, Arrays.stream(lines).collect(Collectors.joining(System.lineSeparator())), actualComment + ) + .contains(lines); + + return this; + } + + public ConstructorAssert noneOfParameterHasAnnotation(final String annotationName) { + actual.getParameters() + .forEach( + param -> Assertions.assertThat(param.getAnnotations()) + .withFailMessage("Parameter %s contains annotation %s while it shouldn't", param.getNameAsString(), annotationName) + .extracting(NodeWithName::getNameAsString) + .doesNotContain(annotationName) + ); + + return this; + } + +} diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/assertions/JavaFileAssert.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/assertions/JavaFileAssert.java index 3189c4086637..e0621b31662e 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/assertions/JavaFileAssert.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/assertions/JavaFileAssert.java @@ -8,6 +8,7 @@ import java.util.List; import java.util.Optional; import java.util.stream.Collectors; +import com.github.javaparser.ast.body.ConstructorDeclaration; import org.assertj.core.api.AbstractAssert; import org.assertj.core.api.Assertions; import org.assertj.core.util.CanIgnoreReturnValue; @@ -59,6 +60,15 @@ public class JavaFileAssert extends AbstractAssert constructorDeclaration = actual.getType(0).getConstructorByParameterTypes(paramTypes); + Assertions.assertThat(constructorDeclaration) + .withFailMessage("No constructor with parameter(s) %s", Arrays.toString(paramTypes)) + .isPresent(); + + return new ConstructorAssert(this, constructorDeclaration.get()); + } + public PropertyAssert hasProperty(final String propertyName) { Optional fieldOptional = actual.getType(0).getMembers().stream() .filter(FieldDeclaration.class::isInstance) diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/assertions/MethodAnnotationAssert.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/assertions/MethodAnnotationAssert.java index 5667e29a4dcb..afcf80f9f97b 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/assertions/MethodAnnotationAssert.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/assertions/MethodAnnotationAssert.java @@ -10,13 +10,32 @@ import com.github.javaparser.ast.expr.AnnotationExpr; public class MethodAnnotationAssert extends AbstractAnnotationAssert { private final MethodAssert methodAssert; + private final ConstructorAssert constructorAssert; protected MethodAnnotationAssert(final MethodAssert methodAssert, final List annotationExpr) { super(annotationExpr); this.methodAssert = methodAssert; + this.constructorAssert = null; + } + + protected MethodAnnotationAssert(final ConstructorAssert constructorAssert, final List annotationExpr) { + super(annotationExpr); + this.constructorAssert = constructorAssert; + this.methodAssert = null; } public MethodAssert toMethod() { + if (methodAssert == null) { + throw new IllegalArgumentException("No method assert for constructor's annotations"); + } return methodAssert; } + + public ConstructorAssert toConstructor() { + if (constructorAssert == null) { + throw new IllegalArgumentException("No constructor assert for method's annotations"); + } + return constructorAssert; + } + } diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/assertions/ParameterAssert.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/assertions/ParameterAssert.java index a6a4a732cc96..adf73c7063a1 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/assertions/ParameterAssert.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/assertions/ParameterAssert.java @@ -10,16 +10,34 @@ import com.github.javaparser.ast.body.Parameter; public class ParameterAssert extends ObjectAssert { private final MethodAssert methodAssert; + private final ConstructorAssert constructorAssert; protected ParameterAssert(final MethodAssert methodAssert, final Parameter parameter) { super(parameter); this.methodAssert = methodAssert; + this.constructorAssert = null; + } + + protected ParameterAssert(final ConstructorAssert constructorAssert, final Parameter parameter) { + super(parameter); + this.constructorAssert = constructorAssert; + this.methodAssert = null; } public MethodAssert toMethod() { + if (methodAssert == null) { + throw new IllegalArgumentException("No method assert for constructor's parameter"); + } return methodAssert; } + public ConstructorAssert toConstructor() { + if (constructorAssert == null) { + throw new IllegalArgumentException("No constructor assert for method's parameter"); + } + return constructorAssert; + } + public ParameterAssert withType(final String expectedType) { Assertions.assertThat(actual.getTypeAsString()) .withFailMessage("Expected parameter to have type %s, but was %s", expectedType, actual.getTypeAsString()) diff --git a/modules/openapi-generator/src/test/resources/bugs/issue_12790.yaml b/modules/openapi-generator/src/test/resources/bugs/issue_12790.yaml new file mode 100644 index 000000000000..78771207c0b4 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/bugs/issue_12790.yaml @@ -0,0 +1,28 @@ +openapi: 3.0.3 +info: + title: Title + version: "1" +paths: + /test: + get: + responses: + 200: + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/TestObject' +components: + schemas: + TestObject: + type: object + properties: + nullableProperty: + type: string + nullable: true + readOnly: true + notNullableProperty: + type: string + readOnly: true + notNullablePropertyNotRO: + type: integer