Skip to content

Commit ee61deb

Browse files
krumbs8749darw-soptim
authored andcommitted
fix: issue #20304 - map siblings (from OAS 3.1) correctly and added corresponding test
1 parent 2f70572 commit ee61deb

File tree

3 files changed

+293
-32
lines changed

3 files changed

+293
-32
lines changed

modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java

Lines changed: 197 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import com.fasterxml.jackson.databind.JsonNode;
2121
import com.fasterxml.jackson.databind.ObjectMapper;
2222
import io.swagger.v3.core.util.AnnotationsUtils;
23+
import io.swagger.v3.core.util.Json;
2324
import io.swagger.v3.oas.models.OpenAPI;
2425
import io.swagger.v3.oas.models.Operation;
2526
import io.swagger.v3.oas.models.PathItem;
@@ -1346,9 +1347,13 @@ public static Schema unaliasSchema(OpenAPI openAPI,
13461347
}
13471348

13481349
/**
1349-
* Get the actual schema from aliases. If the provided schema is not an alias, the schema itself will be returned.
1350+
* Get the actual schema from aliases. If the provided schema is not an alias,
1351+
* the schema itself will be returned. Sibling fields (title, description,
1352+
* example, etc.) are merged onto any aliased-as-model definitions. Only
1353+
* primitive/array/map aliases are fully unwrapped (and have their $ref
1354+
* cleared).
13501355
*
1351-
* @param openAPI OpenAPI document containing the schemas.
1356+
* @param openAPI OpenAPI document containing the schemas
13521357
* @param schema schema (alias or direct reference)
13531358
* @param schemaMappings mappings of external types to be omitted by unaliasing
13541359
* @return actual schema
@@ -1374,48 +1379,207 @@ public static Schema unaliasSchema(OpenAPI openAPI,
13741379
if (!isRefToSchemaWithProperties(schema.get$ref())) {
13751380
once(LOGGER).warn("{} is not defined", schema.get$ref());
13761381
}
1377-
return schema;
1378-
} else if (isEnumSchema(ref)) {
1379-
// top-level enum class
1382+
return schema; // missing definition → leave as is
1383+
}
1384+
1385+
if (isEnumSchema(ref)) {
1386+
// top-level enum class → leave wrapped
13801387
return schema;
13811388
} else if (isArraySchema(ref)) {
13821389
if (isGenerateAliasAsModel(ref)) {
1383-
return schema; // generate a model extending array
1390+
// generate a model extending array ← leave wrapped
1391+
return schema;
13841392
} else {
1385-
return unaliasSchema(openAPI, allSchemas.get(ModelUtils.getSimpleRef(schema.get$ref())),
1386-
schemaMappings);
1393+
// ↳ unwrap into the array’s item type
1394+
Schema itemSchema = ModelUtils.getSchemaItems(ref);
1395+
Schema copyItem = deepCopy(itemSchema); // deep-copy the inner schema to prevent mutation on wrong component
1396+
copyItem.set$ref(null); // clear $ref so we don’t loop on the same ref
1397+
Schema unwrapped = unaliasSchema(openAPI, copyItem, schemaMappings);
1398+
return mergeSiblingFields(schema, unwrapped); // merge wrapper’s siblings
13871399
}
13881400
} else if (isComposedSchema(ref)) {
13891401
return schema;
13901402
} else if (isMapSchema(ref)) {
1391-
if (ref.getProperties() != null && !ref.getProperties().isEmpty()) // has at least one property
1392-
return schema; // treat it as model
1393-
else {
1394-
if (isGenerateAliasAsModel(ref)) {
1395-
return schema; // generate a model extending map
1396-
} else {
1397-
// treat it as a typical map
1398-
return unaliasSchema(openAPI, allSchemas.get(ModelUtils.getSimpleRef(schema.get$ref())),
1399-
schemaMappings);
1400-
}
1403+
boolean hasProps = ref.getProperties() != null && !ref.getProperties().isEmpty();
1404+
if (hasProps || isGenerateAliasAsModel(ref)) {
1405+
// treat as model OR generate a model extending map ← leave wrapped
1406+
return schema;
1407+
} else {
1408+
// ↳ unwrap into the “additionalProperties” value
1409+
Schema addProp = (Schema) ref.getAdditionalProperties();
1410+
Schema copyValue = deepCopy(addProp); // deep-copy the inner map-value schema
1411+
copyValue.set$ref(null); // clear $ref for this inlined type
1412+
Schema unwrapped = unaliasSchema(openAPI, copyValue, schemaMappings);
1413+
return mergeSiblingFields(schema, unwrapped); // merge wrapper’s siblings
14011414
}
1402-
} else if (isObjectSchema(ref)) { // model
1403-
if (ref.getProperties() != null && !ref.getProperties().isEmpty()) { // has at least one property
1415+
} else if (isObjectSchema(ref)) {
1416+
boolean hasProps = ref.getProperties() != null && !ref.getProperties().isEmpty();
1417+
if (hasProps) {
14041418
// TODO we may need to check `hasSelfReference(openAPI, ref)` as a special/edge case:
14051419
// TODO we may also need to revise below to return `ref` instead of schema
14061420
// which is the last reference to the actual model/object
1421+
// hier this is real object model ← leave wrapped
14071422
return schema;
1408-
} else { // free form object (type: object)
1409-
return unaliasSchema(openAPI, allSchemas.get(ModelUtils.getSimpleRef(schema.get$ref())),
1410-
schemaMappings);
1423+
} else {
1424+
// ↳ free-form object (type: object) : same as map-fallback
1425+
Schema copyObj = deepCopy(ref); // deep-copy free-form object
1426+
copyObj.set$ref(null); // clear lingering $ref
1427+
Schema unwrapped = unaliasSchema(openAPI, copyObj, schemaMappings);
1428+
return mergeSiblingFields(schema, unwrapped); // merge wrapper metadata
14111429
}
1412-
} else {
1413-
return unaliasSchema(openAPI, allSchemas.get(ModelUtils.getSimpleRef(schema.get$ref())), schemaMappings);
1430+
}
1431+
1432+
// Priimitive fallback alias
1433+
{
1434+
// ↳ fully unwrap a simple/primitive alias
1435+
Schema copyPrim = deepCopy(ref); // deep-copy primitive definition
1436+
copyPrim.set$ref(null); // must clear $ref to avoid recursion
1437+
Schema unwrapped = unaliasSchema(openAPI, copyPrim, schemaMappings);
1438+
return mergeSiblingFields(schema, unwrapped); // and copy siblings
14141439
}
14151440
}
1441+
1442+
// no $ref → nothing to unwrap
14161443
return schema;
14171444
}
14181445

1446+
/**
1447+
* Copy any non-null “sibling” fields from the original $ref-wrapper onto the actual definition.
1448+
*/
1449+
/**
1450+
* Copy any non-null “sibling” fields from the original $ref-wrapper
1451+
* onto the actual definition. This now handles the full OAS 3.1 set of
1452+
* keywords that may appear alongside a $ref.
1453+
*/
1454+
private static Schema mergeSiblingFields(Schema original, Schema actual) {
1455+
//--- core title/description/example/default → exactly as before
1456+
if (original.getTitle() != null) actual.setTitle(original.getTitle());
1457+
if (original.getDescription() != null) actual.setDescription(original.getDescription());
1458+
if (original.getExample() != null) actual.setExample(original.getExample());
1459+
if (original.getDefault() != null) actual.setDefault(original.getDefault());
1460+
1461+
//--- readOnly/writeOnly/deprecated/nullable → preserve access flags
1462+
if (original.getReadOnly() != null) actual.setReadOnly(original.getReadOnly());
1463+
if (original.getWriteOnly() != null) actual.setWriteOnly(original.getWriteOnly());
1464+
if (original.getDeprecated() != null) actual.setDeprecated(original.getDeprecated());
1465+
if (original.getNullable() != null) actual.setNullable(original.getNullable());
1466+
1467+
//--- numeric intervals
1468+
if (original.getMaximum() != null)
1469+
actual.setMaximum(original.getMaximum()); // preserve max
1470+
1471+
if (original.getExclusiveMaximum() != null)
1472+
actual.setExclusiveMaximum(original.getExclusiveMaximum()); // preserve exclusiveMax
1473+
if (original.getMinimum() != null) {
1474+
actual.setMinimum(original.getMinimum()); // preserve min
1475+
}
1476+
if (original.getExclusiveMinimum() != null)
1477+
actual.setExclusiveMinimum(original.getExclusiveMinimum()); // preserve exclusiveMin
1478+
if (original.getMultipleOf() != null) actual.setMultipleOf(original.getMultipleOf()); // preserve multipleOf
1479+
1480+
//--- string/array length constraints
1481+
if (original.getMaxLength() != null) actual.setMaxLength(original.getMaxLength()); // preserve maxLength
1482+
if (original.getMinLength() != null) actual.setMinLength(original.getMinLength()); // preserve minLength
1483+
if (original.getMaxItems() != null) actual.setMaxItems(original.getMaxItems()); // preserve maxItems
1484+
if (original.getMinItems() != null) actual.setMinItems(original.getMinItems()); // preserve minItems
1485+
1486+
//--- uniqueItems, maxProperties/minProperties → JSON-schema siblings
1487+
if (original.getUniqueItems() != null) actual.setUniqueItems(original.getUniqueItems()); // preserve uniqueItems
1488+
if (original.getMaxProperties() != null)
1489+
actual.setMaxProperties(original.getMaxProperties()); // preserve maxProperties
1490+
if (original.getMinProperties() != null)
1491+
actual.setMinProperties(original.getMinProperties()); // preserve minProperties
1492+
1493+
//--- pattern, enum → constrain values
1494+
if (original.getPattern() != null) actual.setPattern(original.getPattern()); // preserve pattern
1495+
if (original.getEnum() != null) {
1496+
actual.setEnum(new ArrayList<>(original.getEnum())); // preserve enum list
1497+
}
1498+
1499+
//--- required (object-only) → keep required array if present
1500+
if (original.getRequired() != null) {
1501+
actual.setRequired(new ArrayList<>(original.getRequired())); // preserve required props
1502+
}
1503+
1504+
//--- prefixItems & patternProperties (OAS 3.1)
1505+
if (original.getPrefixItems() != null) {
1506+
actual.setPrefixItems(new ArrayList<>(original.getPrefixItems())); // preserve tuple-style items
1507+
}
1508+
if (original.getPatternProperties() != null) {
1509+
actual.setPatternProperties(new LinkedHashMap<>(original.getPatternProperties())); // preserve patternProperties
1510+
}
1511+
1512+
//--- content-encoding/mediaType/schema (OAS 3.1 media-type siblings)
1513+
if (original.getContentEncoding() != null)
1514+
actual.setContentEncoding(original.getContentEncoding()); // preserve content-encoding
1515+
if (original.getContentMediaType() != null)
1516+
actual.setContentMediaType(original.getContentMediaType()); // preserve contentMediaType
1517+
if (original.getContentSchema() != null)
1518+
actual.setContentSchema(deepCopy(original.getContentSchema())); // preserve contentSchema
1519+
1520+
//--- additionalItems / unevaluatedItems (OAS 3.1 array siblings)
1521+
if (original.getAdditionalItems() != null)
1522+
actual.setAdditionalItems(deepCopy(original.getAdditionalItems())); // preserve additionalItems
1523+
if (original.getUnevaluatedItems() != null)
1524+
actual.setUnevaluatedItems(deepCopy(original.getUnevaluatedItems())); // preserve unevaluatedItems
1525+
1526+
//--- propertyNames / unevaluatedProperties (OAS 3.1 object siblings)
1527+
if (original.getPropertyNames() != null)
1528+
actual.setPropertyNames(deepCopy(original.getPropertyNames())); // preserve propertyNames
1529+
if (original.getUnevaluatedProperties() != null)
1530+
actual.setUnevaluatedProperties(deepCopy(original.getUnevaluatedProperties())); // preserve unevaluatedProperties
1531+
1532+
//--- contains / if / then / else (OAS 3.1 conditional siblings)
1533+
if (original.getContains() != null)
1534+
actual.setContains(deepCopy(original.getContains())); // preserve contains
1535+
if (original.getIf() != null) actual.setIf(deepCopy(original.getIf())); // preserve if
1536+
if (original.getThen() != null) actual.setThen(deepCopy(original.getThen())); // preserve then
1537+
if (original.getElse() != null) actual.setElse(deepCopy(original.getElse())); // preserve else
1538+
1539+
//--- dependentSchemas / dependentRequired (OAS 3.1 dependency siblings)
1540+
if (original.getDependentSchemas() != null)
1541+
actual.setDependentSchemas(new LinkedHashMap<>(original.getDependentSchemas())); // preserve dependentSchemas
1542+
if (original.getDependentRequired() != null)
1543+
actual.setDependentRequired(new LinkedHashMap<>(original.getDependentRequired())); // preserve dependentRequired
1544+
1545+
//--- types (OAS 3.1 JSON-schema `type` as array of strings)
1546+
if (original.getTypes() != null)
1547+
actual.setTypes(new LinkedHashSet<>(original.getTypes())); // preserve type array
1548+
1549+
//--- examples (OAS 3.1 multiple examples)
1550+
if (original.getExamples() != null)
1551+
actual.setExamples(new ArrayList<>(original.getExamples())); // preserve examples
1552+
1553+
//--- booleanSchemaValue (3.1 “boolean” schemas)
1554+
if (original.getBooleanSchemaValue() != null)
1555+
actual.setBooleanSchemaValue(original.getBooleanSchemaValue()); // preserve boolean contents
1556+
1557+
//--- xml / externalDocs / discriminator – these are always siblings of any Schema
1558+
if (original.getXml() != null)
1559+
actual.setXml(original.getXml()); // preserve xml config
1560+
if (original.getExternalDocs() != null)
1561+
actual.setExternalDocs(original.getExternalDocs()); // preserve externalDocs
1562+
if (original.getDiscriminator() != null)
1563+
actual.setDiscriminator(original.getDiscriminator()); // preserve discriminator
1564+
1565+
//--- extensions (x-*)
1566+
if (original.getExtensions() != null && !original.getExtensions().isEmpty()) {
1567+
if (actual.getExtensions() == null) {
1568+
actual.setExtensions(new LinkedHashMap<>()); // ensure extensions map exists
1569+
}
1570+
actual.getExtensions().putAll(original.getExtensions()); // copy custom x-extensions
1571+
}
1572+
1573+
return actual;
1574+
}
1575+
1576+
/**
1577+
* Deep-copy via Jackson so we never touch the registry’s original Schema.
1578+
*/
1579+
private static Schema deepCopy(Schema schema) {
1580+
return Json.mapper().convertValue(schema, Schema.class);
1581+
}
1582+
14191583
/**
14201584
* Returns the additionalProperties Schema for the specified input schema.
14211585
* <p>
@@ -2222,8 +2386,8 @@ public static Schema cloneSchema(Schema schema, boolean openapi31) {
22222386
/**
22232387
* Simplifies the schema by removing the oneOfAnyOf if the oneOfAnyOf only contains a single non-null sub-schema
22242388
*
2225-
* @param openAPI OpenAPI
2226-
* @param schema Schema
2389+
* @param openAPI OpenAPI
2390+
* @param schema Schema
22272391
* @param subSchemas The oneOf or AnyOf schemas
22282392
* @return The simplified schema
22292393
*/
@@ -2356,8 +2520,8 @@ public static boolean isUnsupportedSchema(OpenAPI openAPI, Schema schema) {
23562520
/**
23572521
* Copy meta data (e.g. description, default, examples, etc) from one schema to another.
23582522
*
2359-
* @param from From schema
2360-
* @param to To schema
2523+
* @param from From schema
2524+
* @param to To schema
23612525
*/
23622526
public static void copyMetadata(Schema from, Schema to) {
23632527
if (from.getDescription() != null) {
@@ -2415,7 +2579,7 @@ public static void copyMetadata(Schema from, Schema to) {
24152579
* For example, a schema that only has a `description` without any `properties` or `$ref` defined.
24162580
*
24172581
* @param schema the schema
2418-
* @return if the schema is only metadata and not an actual type
2582+
* @return if the schema is only metadata and not an actual type
24192583
*/
24202584
public static boolean isMetadataOnlySchema(Schema schema) {
24212585
return !(schema.get$ref() != null ||
@@ -2437,8 +2601,9 @@ public static boolean isMetadataOnlySchema(Schema schema) {
24372601

24382602
/**
24392603
* Returns true if the OpenAPI specification contains any schemas which are enums.
2440-
* @param openAPI OpenAPI specification
2441-
* @return true if the OpenAPI specification contains any schemas which are enums.
2604+
*
2605+
* @param openAPI OpenAPI specification
2606+
* @return true if the OpenAPI specification contains any schemas which are enums.
24422607
*/
24432608
public static boolean containsEnums(OpenAPI openAPI) {
24442609
Map<String, Schema> schemaMap = getSchemas(openAPI);

modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5006,4 +5006,41 @@ public void testSingleRequestParameter_hasSingleParamTrue() {
50065006
// When & Then
50075007
assertThat(codegenOperation.getHasSingleParam()).isTrue();
50085008
}
5009+
5010+
@Test
5011+
public void testRefSiblingMergingOnAllAliasForms() {
5012+
// 1) load & flatten
5013+
OpenAPI openAPI = TestUtils.parseFlattenSpec(
5014+
"src/test/resources/3_1/issue_20304.yaml"
5015+
);
5016+
new InlineModelResolver().flatten(openAPI);
5017+
5018+
// 2) prepare codegen
5019+
DefaultCodegen codegen = new DefaultCodegen();
5020+
codegen.setOpenAPI(openAPI);
5021+
5022+
// 3) grab all five wrapper props
5023+
Map<String, Schema> props = openAPI.getComponents()
5024+
.getSchemas()
5025+
.get("ModelWithTitledProperties")
5026+
.getProperties();
5027+
5028+
// 4) for each case: [propertyName, expectedTitle, expectedDescription]
5029+
String[][] cases = {
5030+
{"simpleProperty", "Simple-Property-Title", "Simple-Property-Description"},
5031+
{"allOfRefProperty", "All-Of-Ref-Property-Title", "All-Of-Ref-Property-Description"},
5032+
{"arrayRefProperty", "Array-Ref-Property-Title", "Array-Ref-Property-Description"},
5033+
{"mapRefProperty", "Map-Ref-Property-Title", "Map-Ref-Property-Description"},
5034+
{"objectRefProperty", "Object-Ref-Property-Title", "Object-Ref-Property-Description"}
5035+
};
5036+
5037+
for (String[] c : cases) {
5038+
// required flag is irrelevant for merging siblings
5039+
CodegenProperty cp = codegen.fromProperty(c[0], props.get(c[0]), true);
5040+
5041+
// assert that our override‐siblings came through
5042+
assertEquals(c[1], cp.getTitle(), c[0] + " → title");
5043+
assertEquals(c[2], cp.getDescription(), c[0] + " → description");
5044+
}
5045+
}
50095046
}

0 commit comments

Comments
 (0)