[JAVA] Correct generation of schema default values of type object (issue #21051) (#21278)

* Fix Issue #21051

* Renamed Test to avoid being overriden by JUnit import

* Test fix
This commit is contained in:
DavidGrath 2025-05-25 11:34:42 +01:00 committed by GitHub
parent c4dad53455
commit c6a88eaf8e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 258 additions and 2 deletions

View File

@ -18,7 +18,9 @@
package org.openapitools.codegen.languages; package org.openapitools.codegen.languages;
import com.fasterxml.jackson.databind.JsonNode; 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.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
import com.samskivert.mustache.Mustache; import com.samskivert.mustache.Mustache;
@ -1399,7 +1401,74 @@ public abstract class AbstractJavaCodegen extends DefaultCodegen implements Code
return null; return null;
} else if (ModelUtils.isObjectSchema(schema)) { } else if (ModelUtils.isObjectSchema(schema)) {
if (schema.getDefault() != null) { if (schema.getDefault() != null) {
return super.toDefaultValue(schema); try {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("new " + cp.datatypeWithEnum + "()");
Map<String, Schema> 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<Map.Entry<String, JsonNode>> defaultProperties = objectNode.properties();
for (Map.Entry<String, JsonNode> 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; return null;
} else if (ModelUtils.isComposedSchema(schema)) { } else if (ModelUtils.isComposedSchema(schema)) {

View File

@ -17,6 +17,12 @@
package org.openapitools.codegen.java; 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 com.google.common.collect.ImmutableMap;
import io.swagger.parser.OpenAPIParser; import io.swagger.parser.OpenAPIParser;
import io.swagger.v3.oas.models.OpenAPI; 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.assertThat;
import static org.assertj.core.api.Assertions.entry; import static org.assertj.core.api.Assertions.entry;
import static org.assertj.core.api.InstanceOfAssertFactories.FILE; 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.newTempFolder;
import static org.openapitools.codegen.TestUtils.validateJavaSourceFiles; import static org.openapitools.codegen.TestUtils.validateJavaSourceFiles;
import static org.openapitools.codegen.languages.JavaClientCodegen.*; import static org.openapitools.codegen.languages.JavaClientCodegen.*;
@ -3592,4 +3598,71 @@ public class JavaClientCodegenTest {
"public some.pkg.B getsomepkgB() throws ClassCastException {" "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<String, Object> 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<String, FieldDeclaration> 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<Void> {
Map<String, Expression> 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<String, Expression> 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());
}
} }

View File

@ -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'