2020import com .fasterxml .jackson .databind .JsonNode ;
2121import com .fasterxml .jackson .databind .ObjectMapper ;
2222import io .swagger .v3 .core .util .AnnotationsUtils ;
23+ import io .swagger .v3 .core .util .Json ;
2324import io .swagger .v3 .oas .models .OpenAPI ;
2425import io .swagger .v3 .oas .models .Operation ;
2526import 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 );
0 commit comments