From ee61deb0ceaaf49d7d2e4f8912b7e97e269b38d4 Mon Sep 17 00:00:00 2001 From: Ikram Hafidz Date: Mon, 28 Jul 2025 15:19:27 +0200 Subject: [PATCH 1/5] fix: issue #20304 - map siblings (from OAS 3.1) correctly and added corresponding test --- .../codegen/utils/ModelUtils.java | 229 +++++++++++++++--- .../codegen/DefaultCodegenTest.java | 37 +++ .../src/test/resources/3_1/issue_20304.yaml | 59 +++++ 3 files changed, 293 insertions(+), 32 deletions(-) create mode 100644 modules/openapi-generator/src/test/resources/3_1/issue_20304.yaml diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java index 2c79c8a6ca83..5eb87cd41386 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java @@ -20,6 +20,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import io.swagger.v3.core.util.AnnotationsUtils; +import io.swagger.v3.core.util.Json; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.Operation; import io.swagger.v3.oas.models.PathItem; @@ -1346,9 +1347,13 @@ public static Schema unaliasSchema(OpenAPI openAPI, } /** - * Get the actual schema from aliases. If the provided schema is not an alias, the schema itself will be returned. + * Get the actual schema from aliases. If the provided schema is not an alias, + * the schema itself will be returned. Sibling fields (title, description, + * example, etc.) are merged onto any aliased-as-model definitions. Only + * primitive/array/map aliases are fully unwrapped (and have their $ref + * cleared). * - * @param openAPI OpenAPI document containing the schemas. + * @param openAPI OpenAPI document containing the schemas * @param schema schema (alias or direct reference) * @param schemaMappings mappings of external types to be omitted by unaliasing * @return actual schema @@ -1374,48 +1379,207 @@ public static Schema unaliasSchema(OpenAPI openAPI, if (!isRefToSchemaWithProperties(schema.get$ref())) { once(LOGGER).warn("{} is not defined", schema.get$ref()); } - return schema; - } else if (isEnumSchema(ref)) { - // top-level enum class + return schema; // missing definition → leave as is + } + + if (isEnumSchema(ref)) { + // top-level enum class → leave wrapped return schema; } else if (isArraySchema(ref)) { if (isGenerateAliasAsModel(ref)) { - return schema; // generate a model extending array + // generate a model extending array ← leave wrapped + return schema; } else { - return unaliasSchema(openAPI, allSchemas.get(ModelUtils.getSimpleRef(schema.get$ref())), - schemaMappings); + // ↳ unwrap into the array’s item type + Schema itemSchema = ModelUtils.getSchemaItems(ref); + Schema copyItem = deepCopy(itemSchema); // deep-copy the inner schema to prevent mutation on wrong component + copyItem.set$ref(null); // clear $ref so we don’t loop on the same ref + Schema unwrapped = unaliasSchema(openAPI, copyItem, schemaMappings); + return mergeSiblingFields(schema, unwrapped); // merge wrapper’s siblings } } else if (isComposedSchema(ref)) { return schema; } else if (isMapSchema(ref)) { - if (ref.getProperties() != null && !ref.getProperties().isEmpty()) // has at least one property - return schema; // treat it as model - else { - if (isGenerateAliasAsModel(ref)) { - return schema; // generate a model extending map - } else { - // treat it as a typical map - return unaliasSchema(openAPI, allSchemas.get(ModelUtils.getSimpleRef(schema.get$ref())), - schemaMappings); - } + boolean hasProps = ref.getProperties() != null && !ref.getProperties().isEmpty(); + if (hasProps || isGenerateAliasAsModel(ref)) { + // treat as model OR generate a model extending map ← leave wrapped + return schema; + } else { + // ↳ unwrap into the “additionalProperties” value + Schema addProp = (Schema) ref.getAdditionalProperties(); + Schema copyValue = deepCopy(addProp); // deep-copy the inner map-value schema + copyValue.set$ref(null); // clear $ref for this inlined type + Schema unwrapped = unaliasSchema(openAPI, copyValue, schemaMappings); + return mergeSiblingFields(schema, unwrapped); // merge wrapper’s siblings } - } else if (isObjectSchema(ref)) { // model - if (ref.getProperties() != null && !ref.getProperties().isEmpty()) { // has at least one property + } else if (isObjectSchema(ref)) { + boolean hasProps = ref.getProperties() != null && !ref.getProperties().isEmpty(); + if (hasProps) { // TODO we may need to check `hasSelfReference(openAPI, ref)` as a special/edge case: // TODO we may also need to revise below to return `ref` instead of schema // which is the last reference to the actual model/object + // hier this is real object model ← leave wrapped return schema; - } else { // free form object (type: object) - return unaliasSchema(openAPI, allSchemas.get(ModelUtils.getSimpleRef(schema.get$ref())), - schemaMappings); + } else { + // ↳ free-form object (type: object) : same as map-fallback + Schema copyObj = deepCopy(ref); // deep-copy free-form object + copyObj.set$ref(null); // clear lingering $ref + Schema unwrapped = unaliasSchema(openAPI, copyObj, schemaMappings); + return mergeSiblingFields(schema, unwrapped); // merge wrapper metadata } - } else { - return unaliasSchema(openAPI, allSchemas.get(ModelUtils.getSimpleRef(schema.get$ref())), schemaMappings); + } + + // Priimitive fallback alias + { + // ↳ fully unwrap a simple/primitive alias + Schema copyPrim = deepCopy(ref); // deep-copy primitive definition + copyPrim.set$ref(null); // must clear $ref to avoid recursion + Schema unwrapped = unaliasSchema(openAPI, copyPrim, schemaMappings); + return mergeSiblingFields(schema, unwrapped); // and copy siblings } } + + // no $ref → nothing to unwrap return schema; } + /** + * Copy any non-null “sibling” fields from the original $ref-wrapper onto the actual definition. + */ + /** + * Copy any non-null “sibling” fields from the original $ref-wrapper + * onto the actual definition. This now handles the full OAS 3.1 set of + * keywords that may appear alongside a $ref. + */ + private static Schema mergeSiblingFields(Schema original, Schema actual) { + //--- core title/description/example/default → exactly as before + if (original.getTitle() != null) actual.setTitle(original.getTitle()); + if (original.getDescription() != null) actual.setDescription(original.getDescription()); + if (original.getExample() != null) actual.setExample(original.getExample()); + if (original.getDefault() != null) actual.setDefault(original.getDefault()); + + //--- readOnly/writeOnly/deprecated/nullable → preserve access flags + if (original.getReadOnly() != null) actual.setReadOnly(original.getReadOnly()); + if (original.getWriteOnly() != null) actual.setWriteOnly(original.getWriteOnly()); + if (original.getDeprecated() != null) actual.setDeprecated(original.getDeprecated()); + if (original.getNullable() != null) actual.setNullable(original.getNullable()); + + //--- numeric intervals + if (original.getMaximum() != null) + actual.setMaximum(original.getMaximum()); // preserve max + + if (original.getExclusiveMaximum() != null) + actual.setExclusiveMaximum(original.getExclusiveMaximum()); // preserve exclusiveMax + if (original.getMinimum() != null) { + actual.setMinimum(original.getMinimum()); // preserve min + } + if (original.getExclusiveMinimum() != null) + actual.setExclusiveMinimum(original.getExclusiveMinimum()); // preserve exclusiveMin + if (original.getMultipleOf() != null) actual.setMultipleOf(original.getMultipleOf()); // preserve multipleOf + + //--- string/array length constraints + if (original.getMaxLength() != null) actual.setMaxLength(original.getMaxLength()); // preserve maxLength + if (original.getMinLength() != null) actual.setMinLength(original.getMinLength()); // preserve minLength + if (original.getMaxItems() != null) actual.setMaxItems(original.getMaxItems()); // preserve maxItems + if (original.getMinItems() != null) actual.setMinItems(original.getMinItems()); // preserve minItems + + //--- uniqueItems, maxProperties/minProperties → JSON-schema siblings + if (original.getUniqueItems() != null) actual.setUniqueItems(original.getUniqueItems()); // preserve uniqueItems + if (original.getMaxProperties() != null) + actual.setMaxProperties(original.getMaxProperties()); // preserve maxProperties + if (original.getMinProperties() != null) + actual.setMinProperties(original.getMinProperties()); // preserve minProperties + + //--- pattern, enum → constrain values + if (original.getPattern() != null) actual.setPattern(original.getPattern()); // preserve pattern + if (original.getEnum() != null) { + actual.setEnum(new ArrayList<>(original.getEnum())); // preserve enum list + } + + //--- required (object-only) → keep required array if present + if (original.getRequired() != null) { + actual.setRequired(new ArrayList<>(original.getRequired())); // preserve required props + } + + //--- prefixItems & patternProperties (OAS 3.1) + if (original.getPrefixItems() != null) { + actual.setPrefixItems(new ArrayList<>(original.getPrefixItems())); // preserve tuple-style items + } + if (original.getPatternProperties() != null) { + actual.setPatternProperties(new LinkedHashMap<>(original.getPatternProperties())); // preserve patternProperties + } + + //--- content-encoding/mediaType/schema (OAS 3.1 media-type siblings) + if (original.getContentEncoding() != null) + actual.setContentEncoding(original.getContentEncoding()); // preserve content-encoding + if (original.getContentMediaType() != null) + actual.setContentMediaType(original.getContentMediaType()); // preserve contentMediaType + if (original.getContentSchema() != null) + actual.setContentSchema(deepCopy(original.getContentSchema())); // preserve contentSchema + + //--- additionalItems / unevaluatedItems (OAS 3.1 array siblings) + if (original.getAdditionalItems() != null) + actual.setAdditionalItems(deepCopy(original.getAdditionalItems())); // preserve additionalItems + if (original.getUnevaluatedItems() != null) + actual.setUnevaluatedItems(deepCopy(original.getUnevaluatedItems())); // preserve unevaluatedItems + + //--- propertyNames / unevaluatedProperties (OAS 3.1 object siblings) + if (original.getPropertyNames() != null) + actual.setPropertyNames(deepCopy(original.getPropertyNames())); // preserve propertyNames + if (original.getUnevaluatedProperties() != null) + actual.setUnevaluatedProperties(deepCopy(original.getUnevaluatedProperties())); // preserve unevaluatedProperties + + //--- contains / if / then / else (OAS 3.1 conditional siblings) + if (original.getContains() != null) + actual.setContains(deepCopy(original.getContains())); // preserve contains + if (original.getIf() != null) actual.setIf(deepCopy(original.getIf())); // preserve if + if (original.getThen() != null) actual.setThen(deepCopy(original.getThen())); // preserve then + if (original.getElse() != null) actual.setElse(deepCopy(original.getElse())); // preserve else + + //--- dependentSchemas / dependentRequired (OAS 3.1 dependency siblings) + if (original.getDependentSchemas() != null) + actual.setDependentSchemas(new LinkedHashMap<>(original.getDependentSchemas())); // preserve dependentSchemas + if (original.getDependentRequired() != null) + actual.setDependentRequired(new LinkedHashMap<>(original.getDependentRequired())); // preserve dependentRequired + + //--- types (OAS 3.1 JSON-schema `type` as array of strings) + if (original.getTypes() != null) + actual.setTypes(new LinkedHashSet<>(original.getTypes())); // preserve type array + + //--- examples (OAS 3.1 multiple examples) + if (original.getExamples() != null) + actual.setExamples(new ArrayList<>(original.getExamples())); // preserve examples + + //--- booleanSchemaValue (3.1 “boolean” schemas) + if (original.getBooleanSchemaValue() != null) + actual.setBooleanSchemaValue(original.getBooleanSchemaValue()); // preserve boolean contents + + //--- xml / externalDocs / discriminator – these are always siblings of any Schema + if (original.getXml() != null) + actual.setXml(original.getXml()); // preserve xml config + if (original.getExternalDocs() != null) + actual.setExternalDocs(original.getExternalDocs()); // preserve externalDocs + if (original.getDiscriminator() != null) + actual.setDiscriminator(original.getDiscriminator()); // preserve discriminator + + //--- extensions (x-*) + if (original.getExtensions() != null && !original.getExtensions().isEmpty()) { + if (actual.getExtensions() == null) { + actual.setExtensions(new LinkedHashMap<>()); // ensure extensions map exists + } + actual.getExtensions().putAll(original.getExtensions()); // copy custom x-extensions + } + + return actual; + } + + /** + * Deep-copy via Jackson so we never touch the registry’s original Schema. + */ + private static Schema deepCopy(Schema schema) { + return Json.mapper().convertValue(schema, Schema.class); + } + /** * Returns the additionalProperties Schema for the specified input schema. *

@@ -2222,8 +2386,8 @@ public static Schema cloneSchema(Schema schema, boolean openapi31) { /** * Simplifies the schema by removing the oneOfAnyOf if the oneOfAnyOf only contains a single non-null sub-schema * - * @param openAPI OpenAPI - * @param schema Schema + * @param openAPI OpenAPI + * @param schema Schema * @param subSchemas The oneOf or AnyOf schemas * @return The simplified schema */ @@ -2356,8 +2520,8 @@ public static boolean isUnsupportedSchema(OpenAPI openAPI, Schema schema) { /** * Copy meta data (e.g. description, default, examples, etc) from one schema to another. * - * @param from From schema - * @param to To schema + * @param from From schema + * @param to To schema */ public static void copyMetadata(Schema from, Schema to) { if (from.getDescription() != null) { @@ -2415,7 +2579,7 @@ public static void copyMetadata(Schema from, Schema to) { * For example, a schema that only has a `description` without any `properties` or `$ref` defined. * * @param schema the schema - * @return if the schema is only metadata and not an actual type + * @return if the schema is only metadata and not an actual type */ public static boolean isMetadataOnlySchema(Schema schema) { return !(schema.get$ref() != null || @@ -2437,8 +2601,9 @@ public static boolean isMetadataOnlySchema(Schema schema) { /** * Returns true if the OpenAPI specification contains any schemas which are enums. - * @param openAPI OpenAPI specification - * @return true if the OpenAPI specification contains any schemas which are enums. + * + * @param openAPI OpenAPI specification + * @return true if the OpenAPI specification contains any schemas which are enums. */ public static boolean containsEnums(OpenAPI openAPI) { Map schemaMap = getSchemas(openAPI); diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java index e0886f28a796..eccab73082ad 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java @@ -5006,4 +5006,41 @@ public void testSingleRequestParameter_hasSingleParamTrue() { // When & Then assertThat(codegenOperation.getHasSingleParam()).isTrue(); } + + @Test + public void testRefSiblingMergingOnAllAliasForms() { + // 1) load & flatten + OpenAPI openAPI = TestUtils.parseFlattenSpec( + "src/test/resources/3_1/issue_20304.yaml" + ); + new InlineModelResolver().flatten(openAPI); + + // 2) prepare codegen + DefaultCodegen codegen = new DefaultCodegen(); + codegen.setOpenAPI(openAPI); + + // 3) grab all five wrapper props + Map props = openAPI.getComponents() + .getSchemas() + .get("ModelWithTitledProperties") + .getProperties(); + + // 4) for each case: [propertyName, expectedTitle, expectedDescription] + String[][] cases = { + {"simpleProperty", "Simple-Property-Title", "Simple-Property-Description"}, + {"allOfRefProperty", "All-Of-Ref-Property-Title", "All-Of-Ref-Property-Description"}, + {"arrayRefProperty", "Array-Ref-Property-Title", "Array-Ref-Property-Description"}, + {"mapRefProperty", "Map-Ref-Property-Title", "Map-Ref-Property-Description"}, + {"objectRefProperty", "Object-Ref-Property-Title", "Object-Ref-Property-Description"} + }; + + for (String[] c : cases) { + // required flag is irrelevant for merging siblings + CodegenProperty cp = codegen.fromProperty(c[0], props.get(c[0]), true); + + // assert that our override‐siblings came through + assertEquals(c[1], cp.getTitle(), c[0] + " → title"); + assertEquals(c[2], cp.getDescription(), c[0] + " → description"); + } + } } diff --git a/modules/openapi-generator/src/test/resources/3_1/issue_20304.yaml b/modules/openapi-generator/src/test/resources/3_1/issue_20304.yaml new file mode 100644 index 000000000000..58d57c7052fd --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_1/issue_20304.yaml @@ -0,0 +1,59 @@ +# src/test/resources/3_1/issue_20304_siblings.yaml +openapi: 3.1.0 +info: + title: Test siblings + version: 1.0.0 +components: + schemas: + ModelWithTitledProperties: + type: object + properties: + # 1) simple $ref + simpleProperty: + $ref: '#/components/schemas/Inner' + title: Simple-Property-Title + description: Simple-Property-Description + + # 2) allOf wrapping a $ref + allOfRefProperty: + allOf: + - $ref: '#/components/schemas/Inner' + title: All-Of-Ref-Property-Title + description: All-Of-Ref-Property-Description + + # 3) array alias + arrayRefProperty: + $ref: '#/components/schemas/ArrayOfInner' + title: Array-Ref-Property-Title + description: Array-Ref-Property-Description + + # 4) map alias + mapRefProperty: + $ref: '#/components/schemas/MapOfInner' + title: Map-Ref-Property-Title + description: Map-Ref-Property-Description + + # 5) object-model alias + objectRefProperty: + $ref: '#/components/schemas/ObjectInner' + title: Object-Ref-Property-Title + description: Object-Ref-Property-Description + + Inner: + type: string + + ArrayOfInner: + type: array + items: + $ref: '#/components/schemas/Inner' + + MapOfInner: + type: object + additionalProperties: + $ref: '#/components/schemas/Inner' + + ObjectInner: + type: object + properties: + foo: + type: integer \ No newline at end of file From 181456f908983131f3483a6cfcfe9f4dc3d960d3 Mon Sep 17 00:00:00 2001 From: Ikram Hafidz Date: Tue, 29 Jul 2025 16:10:17 +0200 Subject: [PATCH 2/5] fix so that tests passed - ensure merger OAS 3.0 and 3.1 compatible --- .../codegen/utils/ModelUtils.java | 255 +++++++++--------- 1 file changed, 135 insertions(+), 120 deletions(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java index 5eb87cd41386..8b64b094a03b 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java @@ -1390,27 +1390,33 @@ public static Schema unaliasSchema(OpenAPI openAPI, // generate a model extending array ← leave wrapped return schema; } else { - // ↳ unwrap into the array’s item type - Schema itemSchema = ModelUtils.getSchemaItems(ref); - Schema copyItem = deepCopy(itemSchema); // deep-copy the inner schema to prevent mutation on wrong component - copyItem.set$ref(null); // clear $ref so we don’t loop on the same ref - Schema unwrapped = unaliasSchema(openAPI, copyItem, schemaMappings); - return mergeSiblingFields(schema, unwrapped); // merge wrapper’s siblings + // ↳ unwrap the alias but keep the array container + Schema copyArray = deepCopy(ref); // deep-copy the full ArraySchema so we never mutate the shared registry + copyArray.set$ref(null); // clear the container’s own $ref + // recursively unalias its items + Schema inner = ModelUtils.getSchemaItems(copyArray); + Schema unaliasedItem = unaliasSchema(openAPI, inner, schemaMappings); + // do not clear unaliasedItem.$ref – we want downstream to still know this is a component + copyArray.setItems(unaliasedItem); // restore the container pointer + return mergeSiblingFields(schema, copyArray); } } else if (isComposedSchema(ref)) { return schema; } else if (isMapSchema(ref)) { boolean hasProps = ref.getProperties() != null && !ref.getProperties().isEmpty(); if (hasProps || isGenerateAliasAsModel(ref)) { - // treat as model OR generate a model extending map ← leave wrapped + // map‐modeled‐as‐class ← leave wrapped return schema; } else { - // ↳ unwrap into the “additionalProperties” value - Schema addProp = (Schema) ref.getAdditionalProperties(); - Schema copyValue = deepCopy(addProp); // deep-copy the inner map-value schema - copyValue.set$ref(null); // clear $ref for this inlined type - Schema unwrapped = unaliasSchema(openAPI, copyValue, schemaMappings); - return mergeSiblingFields(schema, unwrapped); // merge wrapper’s siblings + // ↳ unwrap the alias but keep the map container + Schema copyMap = deepCopy(ref); // deep-copy the full MapSchema so we never mutate the shared registry + copyMap.set$ref(null); // clear the container’s own $ref + Object addl = copyMap.getAdditionalProperties(); + if (addl instanceof Schema) { + Schema unaliasedValue = unaliasSchema(openAPI, (Schema) addl, schemaMappings); + copyMap.setAdditionalProperties(unaliasedValue); + } + return mergeSiblingFields(schema, copyMap); } } else if (isObjectSchema(ref)) { boolean hasProps = ref.getProperties() != null && !ref.getProperties().isEmpty(); @@ -1422,20 +1428,21 @@ public static Schema unaliasSchema(OpenAPI openAPI, return schema; } else { // ↳ free-form object (type: object) : same as map-fallback - Schema copyObj = deepCopy(ref); // deep-copy free-form object - copyObj.set$ref(null); // clear lingering $ref + Schema copyObj = deepCopy(ref); // deep-copy free-form object + copyObj.set$ref(null); // clear lingering $ref so we don’t recurse Schema unwrapped = unaliasSchema(openAPI, copyObj, schemaMappings); - return mergeSiblingFields(schema, unwrapped); // merge wrapper metadata + return mergeSiblingFields(schema, unwrapped); } } - // Priimitive fallback alias + // Primitive fallback alias { // ↳ fully unwrap a simple/primitive alias - Schema copyPrim = deepCopy(ref); // deep-copy primitive definition - copyPrim.set$ref(null); // must clear $ref to avoid recursion + Schema copyPrim = deepCopy(ref); // deep-copy primitive definition + if (copyPrim == null) return ref; + copyPrim.set$ref(null); // clear its $ref to avoid recursion Schema unwrapped = unaliasSchema(openAPI, copyPrim, schemaMappings); - return mergeSiblingFields(schema, unwrapped); // and copy siblings + return mergeSiblingFields(schema, unwrapped); } } @@ -1443,141 +1450,149 @@ public static Schema unaliasSchema(OpenAPI openAPI, return schema; } - /** - * Copy any non-null “sibling” fields from the original $ref-wrapper onto the actual definition. - */ /** * Copy any non-null “sibling” fields from the original $ref-wrapper - * onto the actual definition. This now handles the full OAS 3.1 set of - * keywords that may appear alongside a $ref. + * onto the actual definition. This covers the full OAS 3.1 spec. */ private static Schema mergeSiblingFields(Schema original, Schema actual) { - //--- core title/description/example/default → exactly as before + // stash away any container‐specific pointers on the "actual" schema + Schema preservedItems = actual.getItems(); + Object preservedAddlProps = actual.getAdditionalProperties(); + Map preservedProps = actual.getProperties(); + + // --- core metadata if (original.getTitle() != null) actual.setTitle(original.getTitle()); if (original.getDescription() != null) actual.setDescription(original.getDescription()); if (original.getExample() != null) actual.setExample(original.getExample()); if (original.getDefault() != null) actual.setDefault(original.getDefault()); - //--- readOnly/writeOnly/deprecated/nullable → preserve access flags + // --- read/write flags & deprecation if (original.getReadOnly() != null) actual.setReadOnly(original.getReadOnly()); if (original.getWriteOnly() != null) actual.setWriteOnly(original.getWriteOnly()); if (original.getDeprecated() != null) actual.setDeprecated(original.getDeprecated()); if (original.getNullable() != null) actual.setNullable(original.getNullable()); - //--- numeric intervals - if (original.getMaximum() != null) - actual.setMaximum(original.getMaximum()); // preserve max - - if (original.getExclusiveMaximum() != null) - actual.setExclusiveMaximum(original.getExclusiveMaximum()); // preserve exclusiveMax - if (original.getMinimum() != null) { - actual.setMinimum(original.getMinimum()); // preserve min - } - if (original.getExclusiveMinimum() != null) - actual.setExclusiveMinimum(original.getExclusiveMinimum()); // preserve exclusiveMin - if (original.getMultipleOf() != null) actual.setMultipleOf(original.getMultipleOf()); // preserve multipleOf - - //--- string/array length constraints - if (original.getMaxLength() != null) actual.setMaxLength(original.getMaxLength()); // preserve maxLength - if (original.getMinLength() != null) actual.setMinLength(original.getMinLength()); // preserve minLength - if (original.getMaxItems() != null) actual.setMaxItems(original.getMaxItems()); // preserve maxItems - if (original.getMinItems() != null) actual.setMinItems(original.getMinItems()); // preserve minItems - - //--- uniqueItems, maxProperties/minProperties → JSON-schema siblings - if (original.getUniqueItems() != null) actual.setUniqueItems(original.getUniqueItems()); // preserve uniqueItems - if (original.getMaxProperties() != null) - actual.setMaxProperties(original.getMaxProperties()); // preserve maxProperties - if (original.getMinProperties() != null) - actual.setMinProperties(original.getMinProperties()); // preserve minProperties - - //--- pattern, enum → constrain values - if (original.getPattern() != null) actual.setPattern(original.getPattern()); // preserve pattern - if (original.getEnum() != null) { - actual.setEnum(new ArrayList<>(original.getEnum())); // preserve enum list - } - - //--- required (object-only) → keep required array if present - if (original.getRequired() != null) { - actual.setRequired(new ArrayList<>(original.getRequired())); // preserve required props - } - - //--- prefixItems & patternProperties (OAS 3.1) - if (original.getPrefixItems() != null) { - actual.setPrefixItems(new ArrayList<>(original.getPrefixItems())); // preserve tuple-style items - } - if (original.getPatternProperties() != null) { - actual.setPatternProperties(new LinkedHashMap<>(original.getPatternProperties())); // preserve patternProperties - } - - //--- content-encoding/mediaType/schema (OAS 3.1 media-type siblings) + // --- numeric constraints + if (original.getMaximum() != null) actual.setMaximum(original.getMaximum()); + if (original.getExclusiveMaximum() != null) actual.setExclusiveMaximum(original.getExclusiveMaximum()); + if (original.getMinimum() != null) actual.setMinimum(original.getMinimum()); + if (original.getExclusiveMinimum() != null) actual.setExclusiveMinimum(original.getExclusiveMinimum()); + if (original.getMultipleOf() != null) actual.setMultipleOf(original.getMultipleOf()); + + // --- length / size constraints + if (original.getMaxLength() != null) actual.setMaxLength(original.getMaxLength()); + if (original.getMinLength() != null) actual.setMinLength(original.getMinLength()); + if (original.getPattern() != null) actual.setPattern(original.getPattern()); + if (original.getMaxItems() != null) actual.setMaxItems(original.getMaxItems()); + if (original.getMinItems() != null) actual.setMinItems(original.getMinItems()); + if (original.getUniqueItems() != null) actual.setUniqueItems(original.getUniqueItems()); + if (original.getMaxProperties() != null) actual.setMaxProperties(original.getMaxProperties()); + if (original.getMinProperties() != null) actual.setMinProperties(original.getMinProperties()); + + // --- enum & required (object-only) + if (original.getEnum() != null) actual.setEnum(new ArrayList<>(original.getEnum())); + if (original.getRequired() != null) actual.setRequired(new ArrayList<>(original.getRequired())); + + // --- OAS 3.1 array siblings + if (original.getAdditionalItems() != null) // tuple-style additionalItems + actual.setAdditionalItems(deepCopy(original.getAdditionalItems())); + if (original.getUnevaluatedItems() != null) // unevaluatedItems + actual.setUnevaluatedItems(deepCopy(original.getUnevaluatedItems())); + if (original.getPrefixItems() != null) // tuple prefixItems + actual.setPrefixItems(new ArrayList<>(original.getPrefixItems())); + + // --- OAS 3.1 object siblings + if (original.getPatternProperties() != null) // patternProperties + actual.setPatternProperties(new LinkedHashMap<>(original.getPatternProperties())); + if (original.getPropertyNames() != null) // propertyNames + actual.setPropertyNames(deepCopy(original.getPropertyNames())); + if (original.getUnevaluatedProperties() != null)// unevaluatedProperties + actual.setUnevaluatedProperties(deepCopy(original.getUnevaluatedProperties())); + + // --- OAS 3.1 conditional / dependency siblings + if (original.getContains() != null) // contains + actual.setContains(deepCopy(original.getContains())); + if (original.getIf() != null) // if + actual.setIf(deepCopy(original.getIf())); + if (original.getThen() != null) // then + actual.setThen(deepCopy(original.getThen())); + if (original.getElse() != null) // else + actual.setElse(deepCopy(original.getElse())); + if (original.getDependentSchemas() != null) // dependentSchemas + actual.setDependentSchemas(new LinkedHashMap<>(original.getDependentSchemas())); + if (original.getDependentRequired() != null)// dependentRequired + actual.setDependentRequired(new LinkedHashMap<>(original.getDependentRequired())); + + // --- OAS 3.1 media-type siblings (for contentEncoding / contentMediaType) if (original.getContentEncoding() != null) - actual.setContentEncoding(original.getContentEncoding()); // preserve content-encoding + actual.setContentEncoding(original.getContentEncoding()); if (original.getContentMediaType() != null) - actual.setContentMediaType(original.getContentMediaType()); // preserve contentMediaType + actual.setContentMediaType(original.getContentMediaType()); if (original.getContentSchema() != null) - actual.setContentSchema(deepCopy(original.getContentSchema())); // preserve contentSchema - - //--- additionalItems / unevaluatedItems (OAS 3.1 array siblings) - if (original.getAdditionalItems() != null) - actual.setAdditionalItems(deepCopy(original.getAdditionalItems())); // preserve additionalItems - if (original.getUnevaluatedItems() != null) - actual.setUnevaluatedItems(deepCopy(original.getUnevaluatedItems())); // preserve unevaluatedItems - - //--- propertyNames / unevaluatedProperties (OAS 3.1 object siblings) - if (original.getPropertyNames() != null) - actual.setPropertyNames(deepCopy(original.getPropertyNames())); // preserve propertyNames - if (original.getUnevaluatedProperties() != null) - actual.setUnevaluatedProperties(deepCopy(original.getUnevaluatedProperties())); // preserve unevaluatedProperties - - //--- contains / if / then / else (OAS 3.1 conditional siblings) - if (original.getContains() != null) - actual.setContains(deepCopy(original.getContains())); // preserve contains - if (original.getIf() != null) actual.setIf(deepCopy(original.getIf())); // preserve if - if (original.getThen() != null) actual.setThen(deepCopy(original.getThen())); // preserve then - if (original.getElse() != null) actual.setElse(deepCopy(original.getElse())); // preserve else - - //--- dependentSchemas / dependentRequired (OAS 3.1 dependency siblings) - if (original.getDependentSchemas() != null) - actual.setDependentSchemas(new LinkedHashMap<>(original.getDependentSchemas())); // preserve dependentSchemas - if (original.getDependentRequired() != null) - actual.setDependentRequired(new LinkedHashMap<>(original.getDependentRequired())); // preserve dependentRequired - - //--- types (OAS 3.1 JSON-schema `type` as array of strings) + actual.setContentSchema(deepCopy(original.getContentSchema())); + + // --- JSON-schema type array (OAS 3.1) if (original.getTypes() != null) - actual.setTypes(new LinkedHashSet<>(original.getTypes())); // preserve type array + actual.setTypes(new LinkedHashSet<>(original.getTypes())); - //--- examples (OAS 3.1 multiple examples) + // --- multiple examples (OAS 3.1) if (original.getExamples() != null) - actual.setExamples(new ArrayList<>(original.getExamples())); // preserve examples + actual.setExamples(new ArrayList<>(original.getExamples())); - //--- booleanSchemaValue (3.1 “boolean” schemas) + // --- boolean schemas (OAS 3.1 “booleanSchemaValue”) if (original.getBooleanSchemaValue() != null) - actual.setBooleanSchemaValue(original.getBooleanSchemaValue()); // preserve boolean contents + actual.setBooleanSchemaValue(original.getBooleanSchemaValue()); - //--- xml / externalDocs / discriminator – these are always siblings of any Schema - if (original.getXml() != null) - actual.setXml(original.getXml()); // preserve xml config - if (original.getExternalDocs() != null) - actual.setExternalDocs(original.getExternalDocs()); // preserve externalDocs - if (original.getDiscriminator() != null) - actual.setDiscriminator(original.getDiscriminator()); // preserve discriminator + // --- always-allowed siblings on any schema + if (original.getXml() != null) actual.setXml(original.getXml()); // XML metadata + if (original.getExternalDocs() != null) actual.setExternalDocs(original.getExternalDocs()); // externalDocs + if (original.getDiscriminator() != null) actual.setDiscriminator(original.getDiscriminator()); // discriminator - //--- extensions (x-*) + // --- finally, any vendor extensions if (original.getExtensions() != null && !original.getExtensions().isEmpty()) { if (actual.getExtensions() == null) { - actual.setExtensions(new LinkedHashMap<>()); // ensure extensions map exists + actual.setExtensions(new LinkedHashMap<>()); // ensure non-null map } - actual.getExtensions().putAll(original.getExtensions()); // copy custom x-extensions + actual.getExtensions().putAll(original.getExtensions()); // copy all x-* } + // restore the three container fields we stashed at the top: + actual.setItems(preservedItems); + actual.setAdditionalProperties(preservedAddlProps); + actual.setProperties(preservedProps); + return actual; } /** * Deep-copy via Jackson so we never touch the registry’s original Schema. + * This version preserves the concrete subtype (ArraySchema, ObjectSchema, etc.), + * which is critical for all the places that cast back to the original schema class. */ - private static Schema deepCopy(Schema schema) { - return Json.mapper().convertValue(schema, Schema.class); + private static T deepCopy(T schema) { + if (schema == null) { + return null; + } + // pull off additionalProperties (could be Boolean or Schema) + Object addl = schema.getAdditionalProperties(); + // clear it so Jackson won't choke + schema.setAdditionalProperties(null); + + // do the normal convertValue into the exact same subtype + T copy = (T) Json.mapper().convertValue(schema, schema.getClass()); + + // restore the original on the source + schema.setAdditionalProperties(addl); + + // 5) put it back on the clone, deep-copying if it's itself a Schema + if (addl instanceof Schema) { + copy.setAdditionalProperties(deepCopy((Schema) addl)); + } else if (addl != null) { + // could be Boolean true/false or other + copy.setAdditionalProperties(addl); + } + + return copy; } /** From 4a314c0d67039f63baee3678dba889f74b7a2135 Mon Sep 17 00:00:00 2001 From: Ikram Hafidz Date: Wed, 30 Jul 2025 15:22:14 +0200 Subject: [PATCH 3/5] fix: ensure tests passed --- .../codegen/utils/ModelUtils.java | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java index 8b64b094a03b..6ab68a412a1d 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java @@ -1420,17 +1420,24 @@ public static Schema unaliasSchema(OpenAPI openAPI, } } else if (isObjectSchema(ref)) { boolean hasProps = ref.getProperties() != null && !ref.getProperties().isEmpty(); - if (hasProps) { + if (!hasProps && (ref.getDefault() != null || ref.getExample() != null)) { + // Free‐form object WITH default/example → keep the $ref but copy defaults up + + // clone the wrapper ($ref-only)… + Schema wrapperCopy = (Schema) deepCopy(schema); + // …then merge all the siblings from the component onto it + return mergeSiblingFields(ref, wrapperCopy); + } else if (hasProps) { // TODO we may need to check `hasSelfReference(openAPI, ref)` as a special/edge case: // TODO we may also need to revise below to return `ref` instead of schema // which is the last reference to the actual model/object // hier this is real object model ← leave wrapped return schema; } else { - // ↳ free-form object (type: object) : same as map-fallback - Schema copyObj = deepCopy(ref); // deep-copy free-form object - copyObj.set$ref(null); // clear lingering $ref so we don’t recurse - Schema unwrapped = unaliasSchema(openAPI, copyObj, schemaMappings); + // Free‐form object WITHOUT default/example → unwrap into inline free-form + Schema copyObj = deepCopy(ref); + copyObj.set$ref(null); + Schema unwrapped = unaliasSchema(openAPI, copyObj, schemaMappings); return mergeSiblingFields(schema, unwrapped); } } @@ -1465,6 +1472,8 @@ private static Schema mergeSiblingFields(Schema original, Schema actual) { if (original.getDescription() != null) actual.setDescription(original.getDescription()); if (original.getExample() != null) actual.setExample(original.getExample()); if (original.getDefault() != null) actual.setDefault(original.getDefault()); + if (original.getType() != null) actual.setType(original.getType()); + if (original.getFormat() != null) actual.setFormat(original.getFormat()); // --- read/write flags & deprecation if (original.getReadOnly() != null) actual.setReadOnly(original.getReadOnly()); @@ -1584,7 +1593,7 @@ private static T deepCopy(T schema) { // restore the original on the source schema.setAdditionalProperties(addl); - // 5) put it back on the clone, deep-copying if it's itself a Schema + // put it back on the clone, deep-copying if it's itself a Schema if (addl instanceof Schema) { copy.setAdditionalProperties(deepCopy((Schema) addl)); } else if (addl != null) { From 7ebb3d0cf466cd4949a5d3e30d678633728fe46d Mon Sep 17 00:00:00 2001 From: Ikram Hafidz Date: Wed, 30 Jul 2025 16:26:09 +0200 Subject: [PATCH 4/5] fix: testAnyType --- .../codegen/utils/ModelUtils.java | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java index 6ab68a412a1d..a5a79cff13a7 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java @@ -1440,15 +1440,24 @@ public static Schema unaliasSchema(OpenAPI openAPI, Schema unwrapped = unaliasSchema(openAPI, copyObj, schemaMappings); return mergeSiblingFields(schema, unwrapped); } - } - - // Primitive fallback alias - { - // ↳ fully unwrap a simple/primitive alias - Schema copyPrim = deepCopy(ref); // deep-copy primitive definition + } else { + boolean isFreeFormAny = + !isArraySchema(ref) && + !isMapSchema(ref) && + !isEnumSchema(ref) && + !isComposedSchema(ref) && + (ref.getProperties() == null || ref.getProperties().isEmpty()) && + ref.getDefault() == null && + ref.getExample() == null && + !isGenerateAliasAsModel(ref); + + if (isFreeFormAny) { + return unaliasSchema(openAPI, ref, schemaMappings); + } + Schema copyPrim = deepCopy(ref); if (copyPrim == null) return ref; - copyPrim.set$ref(null); // clear its $ref to avoid recursion - Schema unwrapped = unaliasSchema(openAPI, copyPrim, schemaMappings); + copyPrim.set$ref(null); // clear its $ref to avoid recursion + Schema unwrapped = unaliasSchema(openAPI, copyPrim, schemaMappings); return mergeSiblingFields(schema, unwrapped); } } From 4b372e27ee67026f419860fa9e19c6539fc4070f Mon Sep 17 00:00:00 2001 From: Ikram Hafidz Date: Wed, 30 Jul 2025 17:22:34 +0200 Subject: [PATCH 5/5] requirement: ran pull request scripts --- .../client/others/rust/reqwest-regression-16119/Cargo.toml | 1 + .../others/rust/reqwest-regression-16119/src/models/parent.rs | 4 ++-- samples/client/petstore/java/okhttp-gson-3.1/api/openapi.yaml | 3 ++- samples/client/petstore/java/okhttp-gson/api/openapi.yaml | 4 ---- .../rust-server-deprecated/output/openapi-v3/api/openapi.yaml | 1 + .../petstore/rust-server/output/openapi-v3/api/openapi.yaml | 1 + 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/samples/client/others/rust/reqwest-regression-16119/Cargo.toml b/samples/client/others/rust/reqwest-regression-16119/Cargo.toml index c035a20d87c3..555e57f9abe1 100644 --- a/samples/client/others/rust/reqwest-regression-16119/Cargo.toml +++ b/samples/client/others/rust/reqwest-regression-16119/Cargo.toml @@ -9,6 +9,7 @@ edition = "2021" [dependencies] serde = { version = "^1.0", features = ["derive"] } +serde_with = { version = "^3.8", default-features = false, features = ["base64", "std", "macros"] } serde_json = "^1.0" serde_repr = "^0.1" url = "^2.5" diff --git a/samples/client/others/rust/reqwest-regression-16119/src/models/parent.rs b/samples/client/others/rust/reqwest-regression-16119/src/models/parent.rs index 31d1ea44857c..0ef49fc1f216 100644 --- a/samples/client/others/rust/reqwest-regression-16119/src/models/parent.rs +++ b/samples/client/others/rust/reqwest-regression-16119/src/models/parent.rs @@ -13,8 +13,8 @@ use serde::{Deserialize, Serialize}; #[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] pub struct Parent { - #[serde(rename = "child", skip_serializing_if = "Option::is_none")] - pub child: Option>, + #[serde(rename = "child", default, with = "::serde_with::rust::double_option", skip_serializing_if = "Option::is_none")] + pub child: Option>>, } impl Parent { diff --git a/samples/client/petstore/java/okhttp-gson-3.1/api/openapi.yaml b/samples/client/petstore/java/okhttp-gson-3.1/api/openapi.yaml index 5c7d496f5756..325f18bb18bc 100644 --- a/samples/client/petstore/java/okhttp-gson-3.1/api/openapi.yaml +++ b/samples/client/petstore/java/okhttp-gson-3.1/api/openapi.yaml @@ -1100,7 +1100,8 @@ components: ref_array_prefix_items: description: | An item that was added to the queue. - items: {} + items: + type: object maxItems: 5 minItems: 3 type: array diff --git a/samples/client/petstore/java/okhttp-gson/api/openapi.yaml b/samples/client/petstore/java/okhttp-gson/api/openapi.yaml index 84cc46562f5a..9caea17d8e19 100644 --- a/samples/client/petstore/java/okhttp-gson/api/openapi.yaml +++ b/samples/client/petstore/java/okhttp-gson/api/openapi.yaml @@ -1859,7 +1859,6 @@ components: Address: additionalProperties: type: integer - default: [] type: object Animal: discriminator: @@ -2258,7 +2257,6 @@ components: StringBooleanMap: additionalProperties: type: boolean - default: [] type: object FileSchemaTestClass: example: @@ -2597,7 +2595,6 @@ components: - $ref: "#/components/schemas/GrandparentAnimal" type: object ArrayOfEnums: - default: [] items: $ref: "#/components/schemas/OuterEnum" type: array @@ -2744,7 +2741,6 @@ components: - type: boolean description: Values of scalar type using anyOf Array: - default: [] description: Values of array type items: $ref: "#/components/schemas/Scalar" diff --git a/samples/server/petstore/rust-server-deprecated/output/openapi-v3/api/openapi.yaml b/samples/server/petstore/rust-server-deprecated/output/openapi-v3/api/openapi.yaml index 326b81e29b6f..6e6ef5fdf609 100644 --- a/samples/server/petstore/rust-server-deprecated/output/openapi-v3/api/openapi.yaml +++ b/samples/server/petstore/rust-server-deprecated/output/openapi-v3/api/openapi.yaml @@ -749,6 +749,7 @@ components: requiredObjectHeader: type: boolean optionalObjectHeader: + format: int32 type: integer required: - requiredObjectHeader diff --git a/samples/server/petstore/rust-server/output/openapi-v3/api/openapi.yaml b/samples/server/petstore/rust-server/output/openapi-v3/api/openapi.yaml index 326b81e29b6f..6e6ef5fdf609 100644 --- a/samples/server/petstore/rust-server/output/openapi-v3/api/openapi.yaml +++ b/samples/server/petstore/rust-server/output/openapi-v3/api/openapi.yaml @@ -749,6 +749,7 @@ components: requiredObjectHeader: type: boolean optionalObjectHeader: + format: int32 type: integer required: - requiredObjectHeader