@@ -1390,27 +1390,33 @@ public static Schema unaliasSchema(OpenAPI openAPI,
13901390 // generate a model extending array ← leave wrapped
13911391 return schema ;
13921392 } else {
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
1393+ // ↳ unwrap the alias but keep the array container
1394+ Schema copyArray = deepCopy (ref ); // deep-copy the full ArraySchema so we never mutate the shared registry
1395+ copyArray .set$ref (null ); // clear the container’s own $ref
1396+ // recursively unalias its items
1397+ Schema inner = ModelUtils .getSchemaItems (copyArray );
1398+ Schema unaliasedItem = unaliasSchema (openAPI , inner , schemaMappings );
1399+ // do not clear unaliasedItem.$ref – we want downstream to still know this is a component
1400+ copyArray .setItems (unaliasedItem ); // restore the container pointer
1401+ return mergeSiblingFields (schema , copyArray );
13991402 }
14001403 } else if (isComposedSchema (ref )) {
14011404 return schema ;
14021405 } else if (isMapSchema (ref )) {
14031406 boolean hasProps = ref .getProperties () != null && !ref .getProperties ().isEmpty ();
14041407 if (hasProps || isGenerateAliasAsModel (ref )) {
1405- // treat as model OR generate a model extending map ← leave wrapped
1408+ // map‐modeled‐as‐class ← leave wrapped
14061409 return schema ;
14071410 } 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
1411+ // ↳ unwrap the alias but keep the map container
1412+ Schema copyMap = deepCopy (ref ); // deep-copy the full MapSchema so we never mutate the shared registry
1413+ copyMap .set$ref (null ); // clear the container’s own $ref
1414+ Object addl = copyMap .getAdditionalProperties ();
1415+ if (addl instanceof Schema ) {
1416+ Schema unaliasedValue = unaliasSchema (openAPI , (Schema ) addl , schemaMappings );
1417+ copyMap .setAdditionalProperties (unaliasedValue );
1418+ }
1419+ return mergeSiblingFields (schema , copyMap );
14141420 }
14151421 } else if (isObjectSchema (ref )) {
14161422 boolean hasProps = ref .getProperties () != null && !ref .getProperties ().isEmpty ();
@@ -1422,162 +1428,171 @@ public static Schema unaliasSchema(OpenAPI openAPI,
14221428 return schema ;
14231429 } else {
14241430 // ↳ 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
1431+ Schema copyObj = deepCopy (ref ); // deep-copy free-form object
1432+ copyObj .set$ref (null ); // clear lingering $ref so we don’t recurse
14271433 Schema unwrapped = unaliasSchema (openAPI , copyObj , schemaMappings );
1428- return mergeSiblingFields (schema , unwrapped ); // merge wrapper metadata
1434+ return mergeSiblingFields (schema , unwrapped );
14291435 }
14301436 }
14311437
1432- // Priimitive fallback alias
1438+ // Primitive fallback alias
14331439 {
14341440 // ↳ 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
1441+ Schema copyPrim = deepCopy (ref ); // deep-copy primitive definition
1442+ if (copyPrim == null ) return ref ;
1443+ copyPrim .set$ref (null ); // clear its $ref to avoid recursion
14371444 Schema unwrapped = unaliasSchema (openAPI , copyPrim , schemaMappings );
1438- return mergeSiblingFields (schema , unwrapped ); // and copy siblings
1445+ return mergeSiblingFields (schema , unwrapped );
14391446 }
14401447 }
14411448
14421449 // no $ref → nothing to unwrap
14431450 return schema ;
14441451 }
14451452
1446- /**
1447- * Copy any non-null “sibling” fields from the original $ref-wrapper onto the actual definition.
1448- */
14491453 /**
14501454 * 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.
1455+ * onto the actual definition. This covers the full OAS 3.1 spec.
14531456 */
14541457 private static Schema mergeSiblingFields (Schema original , Schema actual ) {
1455- //--- core title/description/example/default → exactly as before
1458+ // stash away any container‐specific pointers on the "actual" schema
1459+ Schema preservedItems = actual .getItems ();
1460+ Object preservedAddlProps = actual .getAdditionalProperties ();
1461+ Map <String , Schema > preservedProps = actual .getProperties ();
1462+
1463+ // --- core metadata
14561464 if (original .getTitle () != null ) actual .setTitle (original .getTitle ());
14571465 if (original .getDescription () != null ) actual .setDescription (original .getDescription ());
14581466 if (original .getExample () != null ) actual .setExample (original .getExample ());
14591467 if (original .getDefault () != null ) actual .setDefault (original .getDefault ());
14601468
1461- //--- readOnly/writeOnly/deprecated/nullable → preserve access flags
1469+ // --- read/write flags & deprecation
14621470 if (original .getReadOnly () != null ) actual .setReadOnly (original .getReadOnly ());
14631471 if (original .getWriteOnly () != null ) actual .setWriteOnly (original .getWriteOnly ());
14641472 if (original .getDeprecated () != null ) actual .setDeprecated (original .getDeprecated ());
14651473 if (original .getNullable () != null ) actual .setNullable (original .getNullable ());
14661474
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)
1475+ // --- numeric constraints
1476+ if (original .getMaximum () != null ) actual .setMaximum (original .getMaximum ());
1477+ if (original .getExclusiveMaximum () != null ) actual .setExclusiveMaximum (original .getExclusiveMaximum ());
1478+ if (original .getMinimum () != null ) actual .setMinimum (original .getMinimum ());
1479+ if (original .getExclusiveMinimum () != null ) actual .setExclusiveMinimum (original .getExclusiveMinimum ());
1480+ if (original .getMultipleOf () != null ) actual .setMultipleOf (original .getMultipleOf ());
1481+
1482+ // --- length / size constraints
1483+ if (original .getMaxLength () != null ) actual .setMaxLength (original .getMaxLength ());
1484+ if (original .getMinLength () != null ) actual .setMinLength (original .getMinLength ());
1485+ if (original .getPattern () != null ) actual .setPattern (original .getPattern ());
1486+ if (original .getMaxItems () != null ) actual .setMaxItems (original .getMaxItems ());
1487+ if (original .getMinItems () != null ) actual .setMinItems (original .getMinItems ());
1488+ if (original .getUniqueItems () != null ) actual .setUniqueItems (original .getUniqueItems ());
1489+ if (original .getMaxProperties () != null ) actual .setMaxProperties (original .getMaxProperties ());
1490+ if (original .getMinProperties () != null ) actual .setMinProperties (original .getMinProperties ());
1491+
1492+ // --- enum & required (object-only)
1493+ if (original .getEnum () != null ) actual .setEnum (new ArrayList <>(original .getEnum ()));
1494+ if (original .getRequired () != null ) actual .setRequired (new ArrayList <>(original .getRequired ()));
1495+
1496+ // --- OAS 3.1 array siblings
1497+ if (original .getAdditionalItems () != null ) // tuple-style additionalItems
1498+ actual .setAdditionalItems (deepCopy (original .getAdditionalItems ()));
1499+ if (original .getUnevaluatedItems () != null ) // unevaluatedItems
1500+ actual .setUnevaluatedItems (deepCopy (original .getUnevaluatedItems ()));
1501+ if (original .getPrefixItems () != null ) // tuple prefixItems
1502+ actual .setPrefixItems (new ArrayList <>(original .getPrefixItems ()));
1503+
1504+ // --- OAS 3.1 object siblings
1505+ if (original .getPatternProperties () != null ) // patternProperties
1506+ actual .setPatternProperties (new LinkedHashMap <>(original .getPatternProperties ()));
1507+ if (original .getPropertyNames () != null ) // propertyNames
1508+ actual .setPropertyNames (deepCopy (original .getPropertyNames ()));
1509+ if (original .getUnevaluatedProperties () != null )// unevaluatedProperties
1510+ actual .setUnevaluatedProperties (deepCopy (original .getUnevaluatedProperties ()));
1511+
1512+ // --- OAS 3.1 conditional / dependency siblings
1513+ if (original .getContains () != null ) // contains
1514+ actual .setContains (deepCopy (original .getContains ()));
1515+ if (original .getIf () != null ) // if
1516+ actual .setIf (deepCopy (original .getIf ()));
1517+ if (original .getThen () != null ) // then
1518+ actual .setThen (deepCopy (original .getThen ()));
1519+ if (original .getElse () != null ) // else
1520+ actual .setElse (deepCopy (original .getElse ()));
1521+ if (original .getDependentSchemas () != null ) // dependentSchemas
1522+ actual .setDependentSchemas (new LinkedHashMap <>(original .getDependentSchemas ()));
1523+ if (original .getDependentRequired () != null )// dependentRequired
1524+ actual .setDependentRequired (new LinkedHashMap <>(original .getDependentRequired ()));
1525+
1526+ // --- OAS 3.1 media-type siblings (for contentEncoding / contentMediaType)
15131527 if (original .getContentEncoding () != null )
1514- actual .setContentEncoding (original .getContentEncoding ()); // preserve content-encoding
1528+ actual .setContentEncoding (original .getContentEncoding ());
15151529 if (original .getContentMediaType () != null )
1516- actual .setContentMediaType (original .getContentMediaType ()); // preserve contentMediaType
1530+ actual .setContentMediaType (original .getContentMediaType ());
15171531 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)
1532+ actual .setContentSchema (deepCopy (original .getContentSchema ()));
1533+
1534+ // --- JSON-schema type array (OAS 3.1)
15461535 if (original .getTypes () != null )
1547- actual .setTypes (new LinkedHashSet <>(original .getTypes ())); // preserve type array
1536+ actual .setTypes (new LinkedHashSet <>(original .getTypes ()));
15481537
1549- //--- examples (OAS 3.1 multiple examples )
1538+ // --- multiple examples (OAS 3.1)
15501539 if (original .getExamples () != null )
1551- actual .setExamples (new ArrayList <>(original .getExamples ())); // preserve examples
1540+ actual .setExamples (new ArrayList <>(original .getExamples ()));
15521541
1553- //--- booleanSchemaValue ( 3.1 “boolean” schemas )
1542+ // --- boolean schemas (OAS 3.1 “booleanSchemaValue” )
15541543 if (original .getBooleanSchemaValue () != null )
1555- actual .setBooleanSchemaValue (original .getBooleanSchemaValue ()); // preserve boolean contents
1544+ actual .setBooleanSchemaValue (original .getBooleanSchemaValue ());
15561545
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
1546+ // --- always-allowed siblings on any schema
1547+ if (original .getXml () != null ) actual .setXml (original .getXml ()); // XML metadata
1548+ if (original .getExternalDocs () != null ) actual .setExternalDocs (original .getExternalDocs ()); // externalDocs
1549+ if (original .getDiscriminator () != null ) actual .setDiscriminator (original .getDiscriminator ()); // discriminator
15641550
1565- //--- extensions (x-*)
1551+ // --- finally, any vendor extensions
15661552 if (original .getExtensions () != null && !original .getExtensions ().isEmpty ()) {
15671553 if (actual .getExtensions () == null ) {
1568- actual .setExtensions (new LinkedHashMap <>()); // ensure extensions map exists
1554+ actual .setExtensions (new LinkedHashMap <>()); // ensure non-null map
15691555 }
1570- actual .getExtensions ().putAll (original .getExtensions ()); // copy custom x-extensions
1556+ actual .getExtensions ().putAll (original .getExtensions ()); // copy all x-*
15711557 }
15721558
1559+ // restore the three container fields we stashed at the top:
1560+ actual .setItems (preservedItems );
1561+ actual .setAdditionalProperties (preservedAddlProps );
1562+ actual .setProperties (preservedProps );
1563+
15731564 return actual ;
15741565 }
15751566
15761567 /**
15771568 * Deep-copy via Jackson so we never touch the registry’s original Schema.
1569+ * This version preserves the concrete subtype (ArraySchema, ObjectSchema, etc.),
1570+ * which is critical for all the places that cast back to the original schema class.
15781571 */
1579- private static Schema deepCopy (Schema schema ) {
1580- return Json .mapper ().convertValue (schema , Schema .class );
1572+ private static <T extends Schema > T deepCopy (T schema ) {
1573+ if (schema == null ) {
1574+ return null ;
1575+ }
1576+ // pull off additionalProperties (could be Boolean or Schema)
1577+ Object addl = schema .getAdditionalProperties ();
1578+ // clear it so Jackson won't choke
1579+ schema .setAdditionalProperties (null );
1580+
1581+ // do the normal convertValue into the exact same subtype
1582+ T copy = (T ) Json .mapper ().convertValue (schema , schema .getClass ());
1583+
1584+ // restore the original on the source
1585+ schema .setAdditionalProperties (addl );
1586+
1587+ // 5) put it back on the clone, deep-copying if it's itself a Schema
1588+ if (addl instanceof Schema ) {
1589+ copy .setAdditionalProperties (deepCopy ((Schema ) addl ));
1590+ } else if (addl != null ) {
1591+ // could be Boolean true/false or other
1592+ copy .setAdditionalProperties (addl );
1593+ }
1594+
1595+ return copy ;
15811596 }
15821597
15831598 /**
0 commit comments