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 8a624b0bbf8..e0936fbf95b 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 @@ -18,7 +18,9 @@ package org.openapitools.codegen.languages; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.base.Strings; import com.google.common.collect.Sets; import com.samskivert.mustache.Mustache; @@ -1399,7 +1401,74 @@ public abstract class AbstractJavaCodegen extends DefaultCodegen implements Code return null; } else if (ModelUtils.isObjectSchema(schema)) { if (schema.getDefault() != null) { - return super.toDefaultValue(schema); + try { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append("new " + cp.datatypeWithEnum + "()"); + Map propertySchemas = schema.getProperties(); + if(propertySchemas != null) { + // With `parseOptions.setResolve(true)`, objects with 1 key-value pair are LinkedHashMap and objects with more than 1 are ObjectNode + // When not set, objects of any size are ObjectNode + ObjectMapper objectMapper = new ObjectMapper(); + ObjectNode objectNode; + if(!(schema.getDefault() instanceof ObjectNode)) { + objectNode = objectMapper.valueToTree(schema.getDefault()); + } else { + objectNode = (ObjectNode) schema.getDefault(); + + } + Set> defaultProperties = objectNode.properties(); + for (Map.Entry defaultProperty : defaultProperties) { + String key = defaultProperty.getKey(); + JsonNode value = defaultProperty.getValue(); + Schema propertySchema = propertySchemas.get(key); + if (!value.isValueNode() || propertySchema == null) { //Skip complex objects for now + continue; + } + + String defaultPropertyExpression = null; + if(ModelUtils.isLongSchema(propertySchema)) { + defaultPropertyExpression = value.asText()+"l"; + } else if(ModelUtils.isIntegerSchema(propertySchema)) { + defaultPropertyExpression = value.asText(); + } else if(ModelUtils.isDoubleSchema(propertySchema)) { + defaultPropertyExpression = value.asText()+"d"; + } else if(ModelUtils.isFloatSchema(propertySchema)) { + defaultPropertyExpression = value.asText()+"f"; + } else if(ModelUtils.isNumberSchema(propertySchema)) { + defaultPropertyExpression = "new java.math.BigDecimal(\"" + value.asText() + "\")"; + } else if(ModelUtils.isURISchema(propertySchema)) { + defaultPropertyExpression = "java.net.URI.create(\"" + escapeText(value.asText()) + "\")"; + } else if(ModelUtils.isDateSchema(propertySchema)) { + if("java8".equals(getDateLibrary())) { + defaultPropertyExpression = String.format(Locale.ROOT, "java.time.LocalDate.parse(\"%s\")", value.asText()); + } + } else if(ModelUtils.isDateTimeSchema(propertySchema)) { + if("java8".equals(getDateLibrary())) { + defaultPropertyExpression = String.format(Locale.ROOT, "java.time.OffsetDateTime.parse(\"%s\", %s)", + value.asText(), + "java.time.format.DateTimeFormatter.ISO_ZONED_DATE_TIME.withZone(java.time.ZoneId.systemDefault())"); + } + } else if(ModelUtils.isUUIDSchema(propertySchema)) { + defaultPropertyExpression = "java.util.UUID.fromString(\"" + value.asText() + "\")"; + } else if(ModelUtils.isStringSchema(propertySchema)) { + defaultPropertyExpression = "\"" + value.asText() + "\""; + } else if(ModelUtils.isBooleanSchema(propertySchema)) { + defaultPropertyExpression = value.asText(); + } + if(defaultPropertyExpression != null) { + stringBuilder +// .append(System.lineSeparator()) + .append(".") + .append(toVarName(key)) + .append("(").append(defaultPropertyExpression).append(")"); + } + } + } + return stringBuilder.toString(); + } catch (ClassCastException e) { + LOGGER.error("Can't resolve default value: "+schema.getDefault(), e); + return null; + } } return null; } else if (ModelUtils.isComposedSchema(schema)) { 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 c303d903430..63539efc718 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 @@ -17,6 +17,12 @@ package org.openapitools.codegen.java; +import com.github.javaparser.StaticJavaParser; +import com.github.javaparser.ast.CompilationUnit; +import com.github.javaparser.ast.body.FieldDeclaration; +import com.github.javaparser.ast.expr.Expression; +import com.github.javaparser.ast.expr.MethodCallExpr; +import com.github.javaparser.ast.visitor.*; import com.google.common.collect.ImmutableMap; import io.swagger.parser.OpenAPIParser; import io.swagger.v3.oas.models.OpenAPI; @@ -60,7 +66,7 @@ import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.entry; import static org.assertj.core.api.InstanceOfAssertFactories.FILE; -import static org.openapitools.codegen.CodegenConstants.SERIALIZATION_LIBRARY; +import static org.openapitools.codegen.CodegenConstants.*; import static org.openapitools.codegen.TestUtils.newTempFolder; import static org.openapitools.codegen.TestUtils.validateJavaSourceFiles; import static org.openapitools.codegen.languages.JavaClientCodegen.*; @@ -3592,4 +3598,71 @@ public class JavaClientCodegenTest { "public some.pkg.B getsomepkgB() throws ClassCastException {" ); } + + @Test(description = "Issue #21051") + public void givenComplexObjectHasDefaultValueWhenGenerateThenDefaultAssignmentsAreValid() throws Exception { + File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); + output.deleteOnExit(); + + Map properties = new HashMap<>(); + properties.put(APIS, false); + properties.put(API_DOCS, false); + properties.put(API_TESTS, false); + properties.put(MODEL_DOCS, false); + properties.put(MODEL_TESTS, false); + + Generator generator = new DefaultGenerator(); + CodegenConfigurator configurator = new CodegenConfigurator() + .setInputSpec("src/test/resources/3_1/issue_21051.yaml") + .setGeneratorName("java") + .setAdditionalProperties(properties) + .setOutputDir(output.getAbsolutePath()); + ClientOptInput clientOptInput = configurator.toClientOptInput(); + generator.opts(clientOptInput) + .generate(); + System.out.println("Generator Settings: " + clientOptInput.getGeneratorSettings()); + String outputPath = output.getAbsolutePath() + "/src/main/java/org/openapitools"; + File testModel = new File(outputPath, "/client/model/TestCase.java"); + String fileContent = Files.readString(testModel.toPath()); + + System.out.println(fileContent); + TestUtils.assertValidJavaSourceCode(fileContent); + CompilationUnit compilationUnit = StaticJavaParser.parse(testModel); + Map defaultFields = compilationUnit.getType(0).getFields().stream() + .collect(Collectors.toMap((f) -> f.getVariable(0).getName().asString(), (f) -> f)); + //chain method calls for object initialization + class MethodCallVisitor extends VoidVisitorAdapter { + Map expressionMap = new HashMap<>(); + @Override + public void visit(MethodCallExpr n, Void arg) { + expressionMap.put(n.getNameAsString(), n.getArgument(0)); + if(n.getScope().isPresent()) { + n.getScope().get().accept(this, arg); + } + } + + } + MethodCallVisitor visitor = new MethodCallVisitor(); + defaultFields.get("testComplexInlineObject").getVariable(0).getInitializer().get().asMethodCallExpr() + .accept(visitor, null); + Map expressionMap = visitor.expressionMap; + assertTrue(expressionMap.get("foo").isStringLiteralExpr()); + assertTrue(expressionMap.get("fooInt").isIntegerLiteralExpr()); + assertTrue(expressionMap.get("fooLong").isLongLiteralExpr()); + assertTrue(expressionMap.get("fooBool").isBooleanLiteralExpr()); + assertTrue(expressionMap.get("fooFloat").isDoubleLiteralExpr()); + assertTrue(expressionMap.get("fooDouble").isDoubleLiteralExpr()); + assertTrue(expressionMap.containsKey("_void")); + + assertFalse(expressionMap.containsKey("nonExistentDefault")); + assertFalse(expressionMap.containsKey("nonDefaultedProperty")); + + assertTrue(defaultFields.get("testEmptyInlineObject").getVariable(0).getInitializer().get().isObjectCreationExpr()); + assertTrue(defaultFields.get("testNullableEmptyInlineObject").getVariable(0).getInitializer().get().isObjectCreationExpr()); + assertTrue(defaultFields.get("testNullableComplexInlineObject").getVariable(0).getInitializer().get().isMethodCallExpr()); + assertTrue(defaultFields.get("testEmptyReference").getVariable(0).getInitializer().get().isObjectCreationExpr()); + assertTrue(defaultFields.get("testComplexReference").getVariable(0).getInitializer().get().isMethodCallExpr()); + assertTrue(defaultFields.get("testNullableEmptyReference").getVariable(0).getInitializer().get().isObjectCreationExpr()); + assertTrue(defaultFields.get("testNullableComplexReference").getVariable(0).getInitializer().get().isMethodCallExpr()); + } } \ No newline at end of file diff --git a/modules/openapi-generator/src/test/resources/3_1/issue_21051.yaml b/modules/openapi-generator/src/test/resources/3_1/issue_21051.yaml new file mode 100644 index 00000000000..4f81637890d --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_1/issue_21051.yaml @@ -0,0 +1,114 @@ +# Modified from the original +openapi: 3.1.0 +info: + title: Petstore API + description: Petstore API + version: 1.0.0 +servers: + - url: 'http://localhost' +components: + schemas: + # Not generated - free-form object + EmptyReferenceObject: + type: object + default: {} + ComplexReferenceObject: + type: object + default: + foo: bar + properties: + foo: + type: string + nullable: true + NullableEmptyReferenceObject: + type: object + default: {} + nullable: true + NullableComplexReferenceObject: + type: object + default: + foo: bar + properties: + foo: + type: string + nullable: true + nullable: true + TestCase: + type: object + properties: + testEmptyInlineObject: + type: object + default: {} + testComplexInlineObject: + type: object + default: + foo: bar + fooInt: 28 + fooLong: 5000000000 + fooBool: true + fooFloat: 32.5 + fooDouble: 3332.555 + fooNumber: 120.6 + fooDateTime: "2000-01-01T20:20:20+00:00" + fooUUID: "a0ed70ec-5fe5-415a-be97-a7bf13db9fb6" + void: empty + nonExistentDefault: 27 + properties: + foo: + type: string + nullable: true + fooInt: + type: integer + nullable: true + fooLong: + type: integer + format: int64 + nullable: true + fooBool: + type: boolean + fooFloat: + type: number + format: float + nullable: true + fooDouble: + type: number + format: double + nullable: true + fooNumber: + type: number + nullable: true + fooDateTime: + type: string + format: date-time + nullable: true + fooUUID: + type: string + format: uuid + nullable: true + void: # Java keyword + type: string + nullable: true + nonDefaultedProperty: + type: string + nullable: true + testNullableEmptyInlineObject: + type: object + default: {} + nullable: true + testNullableComplexInlineObject: + type: object + default: + foo: bar + properties: + foo: + type: string + nullable: true + nullable: true + testEmptyReference: + $ref: '#/components/schemas/EmptyReferenceObject' + testComplexReference: + $ref: '#/components/schemas/ComplexReferenceObject' + testNullableEmptyReference: + $ref: '#/components/schemas/NullableEmptyReferenceObject' + testNullableComplexReference: + $ref: '#/components/schemas/NullableComplexReferenceObject'