Skip to content

Commit 1ec1f1a

Browse files
authored
[BUG][JAVA] Fix generation of complex default values (fixes #24993) (#25001)
* feat: add support for complex default values in code generation * fix: update default values in API documentation * fix: update default values in API documentation and model classes * build: move jakarta_annotation_version to ext block * fix: add missing newline characters in multiple files * feat: add support for custom date libraries in default value generation * fix: update default value generation for nullable fields * fix: update default value generation for nullable fields * fix: handle optional nullable fields without default values * fix: handle optional nullable fields without default values * chore: fix format * chore: fix format * fix: handle recursive composed schemas and default values for optional containers * fix: handle recursive composed schemas and default values for optional containers * fix: handle recursive composed schemas and default values for optional containers
1 parent 069e2be commit 1ec1f1a

93 files changed

Lines changed: 1078 additions & 265 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractJavaCodegen.java‎

Lines changed: 62 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,7 @@
1818
package org.openapitools.codegen.languages;
1919

2020
import com.fasterxml.jackson.databind.JsonNode;
21-
import com.fasterxml.jackson.databind.ObjectMapper;
2221
import com.fasterxml.jackson.databind.node.ArrayNode;
23-
import com.fasterxml.jackson.databind.node.ObjectNode;
2422
import com.google.common.base.Strings;
2523
import com.google.common.collect.ImmutableMap;
2624
import com.google.common.collect.Sets;
@@ -87,7 +85,6 @@ public abstract class AbstractJavaCodegen extends DefaultCodegen implements Code
8785
private final Logger LOGGER = LoggerFactory.getLogger(AbstractJavaCodegen.class);
8886
private static final String ARTIFACT_VERSION_DEFAULT_VALUE = "1.0.0";
8987
private static final ZoneId UTC = ZoneId.of("UTC");
90-
9188
public static final String DEFAULT_LIBRARY = "<default>";
9289
public static final String DATE_LIBRARY = "dateLibrary";
9390
public static final String SUPPORT_ASYNC = "supportAsync";
@@ -1442,7 +1439,17 @@ public String toArrayDefaultValue(CodegenProperty cp, Schema schema) {
14421439

14431440
@Override
14441441
public String toDefaultValue(CodegenProperty cp, Schema schema) {
1445-
schema = ModelUtils.getReferencedSchema(this.openAPI, schema);
1442+
Schema originalSchema = schema;
1443+
Schema resolvedSchema = ModelUtils.getReferencedSchema(this.openAPI, schema);
1444+
if (hasOptionalNullableExplicitNullDefault(cp, originalSchema, resolvedSchema)) {
1445+
return null;
1446+
}
1447+
String complexDefault = renderComplexDefaultValue(cp, originalSchema, resolvedSchema);
1448+
if (complexDefault != null) {
1449+
return complexDefault;
1450+
}
1451+
1452+
schema = resolvedSchema;
14461453
if (ModelUtils.isArraySchema(schema)) {
14471454
if (defaultToEmptyContainer) {
14481455
// if default to empty container option is set, respect the default values provided in the spec
@@ -1564,7 +1571,7 @@ public String toDefaultValue(CodegenProperty cp, Schema schema) {
15641571
return null;
15651572
} else if (ModelUtils.isObjectSchema(schema)) {
15661573
if (schema.getDefault() != null) {
1567-
return toObjectDefaultValue(cp, schema.getDefault(), schema.getProperties());
1574+
return renderComplexDefaultValue(cp, schema, schema);
15681575
}
15691576
return null;
15701577
} else if (ModelUtils.isComposedSchema(schema)) {
@@ -1576,11 +1583,11 @@ public String toDefaultValue(CodegenProperty cp, Schema schema) {
15761583
// default (e.g. `{"one":"one"}`) as Java, which does not compile (see #23795).
15771584
Map<String, Schema> propertySchemas = getComposedSchemaProperties(schema);
15781585
if (!propertySchemas.isEmpty()) {
1579-
return toObjectDefaultValue(cp, schema.getDefault(), propertySchemas);
1586+
return renderComplexDefaultValue(cp, schema, schema);
15801587
}
15811588
// No object properties resolved: the composition wraps a non-object, e.g. an `allOf`
15821589
// to an enum or scalar (`allOf: [{$ref: '#/.../CurrencyCode'}]` + sibling `default`).
1583-
// There is nothing to build via toObjectDefaultValue, so defer to the base behavior,
1590+
// There is nothing to build via the complex default renderer, so defer to the base behavior,
15841591
// which emits the raw default for later enum var-name / scalar conversion. Returning
15851592
// null here dropped the default and regressed enum defaults (see #24384).
15861593
return super.toDefaultValue(schema);
@@ -1591,102 +1598,33 @@ public String toDefaultValue(CodegenProperty cp, Schema schema) {
15911598
return super.toDefaultValue(schema);
15921599
}
15931600

1594-
/**
1595-
* Renders the default value of an object-typed property as a Java fluent builder expression, e.g.
1596-
* {@code new Pet().name("doggie").id(1l)}. Only scalar (value node) default properties for which a
1597-
* matching property schema is known are rendered; nested objects are skipped.
1598-
*
1599-
* @param cp the codegen property carrying the target Java type ({@code datatypeWithEnum})
1600-
* @param defaultValue the raw default value from the schema (a {@code Map}/{@code ObjectNode})
1601-
* @param propertySchemas the resolved property schemas used to type each default entry
1602-
* @return the Java expression, or {@code null} if it cannot be resolved
1603-
*/
1604-
private String toObjectDefaultValue(CodegenProperty cp, Object defaultValue, Map<String, Schema> propertySchemas) {
1605-
try {
1606-
StringBuilder stringBuilder = new StringBuilder();
1607-
stringBuilder.append("new " + cp.datatypeWithEnum + "()");
1608-
if (propertySchemas != null) {
1609-
// With `parseOptions.setResolve(true)`, objects with 1 key-value pair are LinkedHashMap and objects with more than 1 are ObjectNode
1610-
// When not set, objects of any size are ObjectNode
1611-
ObjectMapper objectMapper = new ObjectMapper();
1612-
ObjectNode objectNode;
1613-
if(!(defaultValue instanceof ObjectNode)) {
1614-
objectNode = objectMapper.valueToTree(defaultValue);
1615-
} else {
1616-
objectNode = (ObjectNode) defaultValue;
1601+
private String renderComplexDefaultValue(CodegenProperty property, Schema schema, Schema resolvedSchema) {
1602+
JavaDefaultValueRenderer.Context context = new JavaDefaultValueRenderer.Context(
1603+
openAPI,
1604+
dateLibrary,
1605+
useOneOfInterfaces,
1606+
this::escapeText,
1607+
this::toVarName,
1608+
this::toEnumVarName,
1609+
this::fromProperty,
1610+
this::getDefaultCollectionType,
1611+
this::getComposedSchemaProperties);
1612+
return new JavaDefaultValueRenderer(context).render(property, schema, resolvedSchema);
1613+
}
16171614

1618-
}
1619-
Set<Map.Entry<String, JsonNode>> defaultProperties = objectNode.properties();
1620-
for (Map.Entry<String, JsonNode> defaultProperty : defaultProperties) {
1621-
String key = defaultProperty.getKey();
1622-
JsonNode value = defaultProperty.getValue();
1623-
Schema propertySchema = propertySchemas.get(key);
1624-
if (!value.isValueNode() || propertySchema == null) { //Skip complex objects for now
1625-
continue;
1626-
}
1615+
private boolean hasOptionalNullableExplicitNullDefault(CodegenProperty property, Schema originalSchema,
1616+
Schema resolvedSchema) {
1617+
return openApiNullable && property != null && !property.required && property.isNullable
1618+
&& isExplicitNullDefault(originalSchema, resolvedSchema);
1619+
}
16271620

1628-
String defaultPropertyExpression = null;
1629-
if(ModelUtils.isEnumSchema(ModelUtils.getReferencedSchema(this.openAPI, propertySchema))) {
1630-
// Enum-typed property: render the enum constant (e.g. `OutputFormat.OrderEnum.SIMILARITY`)
1631-
// rather than a raw quoted string, which would not compile (see #24298).
1632-
CodegenProperty enumProperty = fromProperty(key, propertySchema);
1633-
String enumType = enumProperty.isEnum
1634-
// an inline enum is generated as a nested class of the containing object type
1635-
? cp.datatypeWithEnum + "." + enumProperty.datatypeWithEnum
1636-
// a `$ref` to a named enum is a top-level type
1637-
: enumProperty.datatypeWithEnum;
1638-
defaultPropertyExpression = enumType + "." + toEnumVarName(value.asText(), enumProperty.dataType);
1639-
} else if(ModelUtils.isLongSchema(propertySchema)) {
1640-
defaultPropertyExpression = value.asText()+"l";
1641-
} else if(ModelUtils.isIntegerSchema(propertySchema)) {
1642-
defaultPropertyExpression = value.asText();
1643-
} else if(ModelUtils.isDoubleSchema(propertySchema)) {
1644-
defaultPropertyExpression = value.asText()+"d";
1645-
} else if(ModelUtils.isFloatSchema(propertySchema)) {
1646-
defaultPropertyExpression = value.asText()+"f";
1647-
} else if(ModelUtils.isNumberSchema(propertySchema)) {
1648-
defaultPropertyExpression = "new java.math.BigDecimal(\"" + value.asText() + "\")";
1649-
} else if(ModelUtils.isURISchema(propertySchema)) {
1650-
defaultPropertyExpression = "java.net.URI.create(\"" + escapeText(value.asText()) + "\")";
1651-
} else if(ModelUtils.isDateSchema(propertySchema)) {
1652-
if("java8".equals(getDateLibrary())) {
1653-
defaultPropertyExpression = String.format(Locale.ROOT, "java.time.LocalDate.parse(\"%s\")", value.asText());
1654-
}
1655-
} else if(ModelUtils.isDateTimeSchema(propertySchema)) {
1656-
if("java8".equals(getDateLibrary())) {
1657-
defaultPropertyExpression = String.format(Locale.ROOT, "java.time.OffsetDateTime.parse(\"%s\", %s)",
1658-
value.asText(),
1659-
"java.time.format.DateTimeFormatter.ISO_ZONED_DATE_TIME.withZone(java.time.ZoneId.systemDefault())");
1660-
}
1661-
} else if(ModelUtils.isTimeLocalSchema(propertySchema)) {
1662-
if("java8".equals(getDateLibrary())) {
1663-
defaultPropertyExpression = String.format(Locale.ROOT, "java.time.LocalTime.parse(\"%s\")", value.asText());
1664-
}
1665-
} else if(ModelUtils.isDateTimeLocalSchema(propertySchema)) {
1666-
if("java8".equals(getDateLibrary())) {
1667-
defaultPropertyExpression = String.format(Locale.ROOT, "java.time.LocalDateTime.parse(\"%s\")", value.asText());
1668-
}
1669-
} else if(ModelUtils.isUUIDSchema(propertySchema)) {
1670-
defaultPropertyExpression = "java.util.UUID.fromString(\"" + value.asText() + "\")";
1671-
} else if(ModelUtils.isStringSchema(propertySchema)) {
1672-
defaultPropertyExpression = "\"" + value.asText() + "\"";
1673-
} else if(ModelUtils.isBooleanSchema(propertySchema)) {
1674-
defaultPropertyExpression = value.asText();
1675-
}
1676-
if(defaultPropertyExpression != null) {
1677-
stringBuilder
1678-
// .append(System.lineSeparator())
1679-
.append(".")
1680-
.append(toVarName(key))
1681-
.append("(").append(defaultPropertyExpression).append(")");
1682-
}
1683-
}
1684-
}
1685-
return stringBuilder.toString();
1686-
} catch (ClassCastException e) {
1687-
LOGGER.error("Can't resolve default value: "+defaultValue, e);
1688-
return null;
1689-
}
1621+
private boolean isExplicitNullDefault(Schema originalSchema, Schema resolvedSchema) {
1622+
return isNullNode(originalSchema == null ? null : originalSchema.getDefault())
1623+
|| isNullNode(resolvedSchema == null ? null : resolvedSchema.getDefault());
1624+
}
1625+
1626+
private boolean isNullNode(Object defaultValue) {
1627+
return defaultValue instanceof JsonNode && ((JsonNode) defaultValue).isNull();
16901628
}
16911629

16921630
/**
@@ -1702,15 +1640,31 @@ private Map<String, Schema> getComposedSchemaProperties(Schema schema) {
17021640
if (schema.getProperties() != null) {
17031641
propertySchemas.putAll(schema.getProperties());
17041642
}
1705-
if (schema.getAllOf() != null) {
1706-
for (Object member : schema.getAllOf()) {
1707-
Schema resolved = ModelUtils.getReferencedSchema(this.openAPI, (Schema) member);
1708-
if (resolved != null && resolved.getProperties() != null) {
1709-
propertySchemas.putAll(resolved.getProperties());
1710-
}
1643+
Set<Schema> visitedSchemas = Collections.newSetFromMap(new IdentityHashMap<>());
1644+
visitedSchemas.add(schema);
1645+
addComposedSchemaProperties(propertySchemas, schema.getAllOf(), visitedSchemas);
1646+
addComposedSchemaProperties(propertySchemas, schema.getOneOf(), visitedSchemas);
1647+
addComposedSchemaProperties(propertySchemas, schema.getAnyOf(), visitedSchemas);
1648+
return propertySchemas;
1649+
}
1650+
1651+
private void addComposedSchemaProperties(Map<String, Schema> propertySchemas, List<Schema> members,
1652+
Set<Schema> visitedSchemas) {
1653+
if (members == null) {
1654+
return;
1655+
}
1656+
for (Schema member : members) {
1657+
Schema resolved = ModelUtils.getReferencedSchema(this.openAPI, member);
1658+
if (resolved == null || !visitedSchemas.add(resolved)) {
1659+
continue;
17111660
}
1661+
if (resolved.getProperties() != null) {
1662+
propertySchemas.putAll(resolved.getProperties());
1663+
}
1664+
addComposedSchemaProperties(propertySchemas, resolved.getAllOf(), visitedSchemas);
1665+
addComposedSchemaProperties(propertySchemas, resolved.getOneOf(), visitedSchemas);
1666+
addComposedSchemaProperties(propertySchemas, resolved.getAnyOf(), visitedSchemas);
17121667
}
1713-
return propertySchemas;
17141668
}
17151669

17161670
private String getDefaultCollectionType(Schema schema) {

0 commit comments

Comments
 (0)