From 9658c05e7edff183cdd7925b35dc547ef0483867 Mon Sep 17 00:00:00 2001 From: Arieh Schneier <15041913+AriehSchneier@users.noreply.github.com> Date: Wed, 14 Jan 2026 11:47:16 +1100 Subject: [PATCH 1/4] When models are deduplicated, in go, create an alias type with the original name --- .../openapitools/codegen/CodegenProperty.java | 6 + .../openapitools/codegen/DefaultCodegen.java | 9 + .../codegen/InlineModelResolver.java | 2 + .../codegen/languages/AbstractGoCodegen.java | 50 ++++++ .../main/resources/go/model_simple.mustache | 7 + .../openapitools/codegen/go/GoModelTest.java | 169 ++++++++++++++++++ .../3_0/inline-deduplicated-schemas.yaml | 144 +++++++++++++++ 7 files changed, 387 insertions(+) create mode 100644 modules/openapi-generator/src/test/resources/3_0/inline-deduplicated-schemas.yaml diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenProperty.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenProperty.java index c854389be7b2..09eddfc86ad2 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenProperty.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenProperty.java @@ -187,6 +187,12 @@ public class CodegenProperty implements Cloneable, IJsonSchemaValidationProperti public boolean isDiscriminator; public boolean isNew; // true when this property overrides an inherited property public Boolean isOverridden; // true if the property is a parent property (not defined in child/current schema) + /** + * The type alias name to use when this property references a deduplicated inline model. + * When non-null, code generators may emit a type alias declaration. + */ + @Getter @Setter + public String dataTypeAlias; @Getter @Setter public List _enum; @Getter @Setter diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java index 2002667a31ce..86e2edc0cda7 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java @@ -4193,6 +4193,15 @@ public CodegenProperty fromProperty(String name, Schema p, boolean required, boo String type = getSchemaType(p); setNonArrayMapProperty(property, type); property.isModel = (ModelUtils.isComposedSchema(referencedSchema) || ModelUtils.isObjectSchema(referencedSchema)) && ModelUtils.isModel(referencedSchema); + + // Check if this property is reusing a model type that was generated/deduplicated by InlineModelResolver + // InlineModelResolver marks deduplicated schemas with x-alias-name vendor extension + if (p.get$ref() != null && original != null && original.getExtensions() != null) { + String dedupedName = (String) original.getExtensions().get("x-alias-name"); + if (dedupedName != null && ModelUtils.isModel(referencedSchema)) { + property.dataTypeAlias = toModelName(dedupedName); + } + } } // restore original schema with default value, nullable, readonly etc diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/InlineModelResolver.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/InlineModelResolver.java index 777fed9b2cb4..1fdb94f49bf7 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/InlineModelResolver.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/InlineModelResolver.java @@ -998,6 +998,8 @@ private Schema makeSchemaInComponents(String name, Schema schema) { Schema refSchema; if (existing != null) { refSchema = new Schema().$ref(existing); + // Store the name this schema would have had if not deduplicated + refSchema.addExtension("x-alias-name", name); } else { if (resolveInlineEnums && schema.getEnum() != null && schema.getEnum().size() > 0) { LOGGER.warn("Model " + name + " promoted to its own schema due to resolveInlineEnums=true"); diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractGoCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractGoCodegen.java index 16b909eab448..b7711d709e56 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractGoCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractGoCodegen.java @@ -736,6 +736,21 @@ public void postProcessModelProperty(CodegenModel model, CodegenProperty propert property.vendorExtensions.put("x-golang-is-container", true); } } + + private void swapDataTypeAndAlias(CodegenProperty property, Map typeAliasesMap) { + if (property.dataTypeAlias != null) { + String dedupedType = property.dataType; + String aliasName = property.dataTypeAlias; + // Swap: field uses the alias, alias definition points to deduplicated type + property.dataType = aliasName; + property.dataTypeAlias = dedupedType; + + // Collect type alias (after swap, dataType is alias name, dataTypeAlias is target) + if (!property.isContainer && !typeAliasesMap.containsKey(property.dataType)) { + typeAliasesMap.put(property.dataType, property.dataTypeAlias); + } + } + } @Override public ModelsMap postProcessModels(ModelsMap objs) { @@ -775,7 +790,25 @@ public ModelsMap postProcessModels(ModelsMap objs) { codegenProperties.addAll(inheritedProperties); } + // Collect reused model properties for type alias generation + Map typeAliasesMap = new LinkedHashMap<>(); + for (CodegenProperty cp : codegenProperties) { + // Swap dataType and dataTypeAlias so fields use the alias name + swapDataTypeAndAlias(cp, typeAliasesMap); + + // Also swap for array items and update the array's dataType + if (cp.items != null && cp.items.dataTypeAlias != null) { + String oldItemsDataType = cp.items.dataType; + swapDataTypeAndAlias(cp.items, typeAliasesMap); + String newItemsDataType = cp.items.dataType; + + // Update the array's dataType to use the new items dataType + if (cp.dataType != null && cp.dataType.contains(oldItemsDataType)) { + cp.dataType = cp.dataType.replace(oldItemsDataType, newItemsDataType); + } + } + if (!addedTimeImport && ("time.Time".equals(cp.dataType) || (cp.items != null && "time.Time".equals(cp.items.complexType)))) { imports.add(createMapping("import", "time")); addedTimeImport = true; @@ -855,6 +888,23 @@ public ModelsMap postProcessModels(ModelsMap objs) { if (generateUnmarshalJSON) { model.vendorExtensions.putIfAbsent("x-go-generate-unmarshal-json", true); } + + // Convert type aliases map to list for template usage + if (!typeAliasesMap.isEmpty()) { + List> typeAliases = new ArrayList<>(); + for (Map.Entry entry : typeAliasesMap.entrySet()) { + if (!entry.getKey().equals(entry.getValue())) { + Map aliasMap = new HashMap<>(); + aliasMap.put("aliasName", entry.getKey()); + aliasMap.put("originalType", entry.getValue()); + typeAliases.add(aliasMap); + } + } + if (!typeAliases.isEmpty()) { + model.vendorExtensions.put("x-go-type-aliases", typeAliases); + model.vendorExtensions.put("x-go-has-type-aliases", true); + } + } } // recursively add import for mapping one type to multiple imports diff --git a/modules/openapi-generator/src/main/resources/go/model_simple.mustache b/modules/openapi-generator/src/main/resources/go/model_simple.mustache index 2690fb9bbf5d..594e88d5c204 100644 --- a/modules/openapi-generator/src/main/resources/go/model_simple.mustache +++ b/modules/openapi-generator/src/main/resources/go/model_simple.mustache @@ -1,3 +1,10 @@ +{{#vendorExtensions.x-go-has-type-aliases}} +// Type aliases for reused model types +{{#vendorExtensions.x-go-type-aliases}} +type {{aliasName}} = {{originalType}} +{{/vendorExtensions.x-go-type-aliases}} + +{{/vendorExtensions.x-go-has-type-aliases}} // checks if the {{classname}} type satisfies the MappedNullable interface at compile time var _ MappedNullable = &{{classname}}{} diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/go/GoModelTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/go/GoModelTest.java index e0829f2e0a6f..43a867e62ec5 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/go/GoModelTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/go/GoModelTest.java @@ -24,13 +24,18 @@ import org.openapitools.codegen.CodegenModel; import org.openapitools.codegen.CodegenProperty; import org.openapitools.codegen.DefaultCodegen; +import org.openapitools.codegen.InlineModelResolver; import org.openapitools.codegen.TestUtils; import org.openapitools.codegen.languages.GoClientCodegen; +import org.openapitools.codegen.model.ModelMap; +import org.openapitools.codegen.model.ModelsMap; import org.testng.Assert; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; @SuppressWarnings("static-method") public class GoModelTest { @@ -324,4 +329,168 @@ public void modelNameMappingsTest(String name, String expectedName, String expec Assert.assertEquals(cm.name, name); Assert.assertEquals(cm.classname, expectedName); } + + @Test(description = "test that direct $ref usage does NOT create aliases") + public void directRefNoAliasTest() { + final Schema phoneNumberSchema = new Schema() + .type("object") + .addProperty("countryCode", new StringSchema()) + .addProperty("number", new StringSchema()) + .addRequiredItem("number"); + + final Schema personSchema = new Schema() + .type("object") + .addProperty("name", new StringSchema()) + .addProperty("mobile", new Schema().$ref("#/components/schemas/PhoneNumber")) + .addProperty("home", new Schema().$ref("#/components/schemas/PhoneNumber")) + .addRequiredItem("name"); + + final DefaultCodegen codegen = new GoClientCodegen(); + OpenAPI openAPI = TestUtils.createOpenAPI(); + openAPI.getComponents().addSchemas("PhoneNumber", phoneNumberSchema); + openAPI.getComponents().addSchemas("Person", personSchema); + codegen.setOpenAPI(openAPI); + + final CodegenModel personModel = codegen.fromModel("Person", personSchema); + Assert.assertEquals(personModel.name, "Person"); + Assert.assertEquals(personModel.vars.size(), 3); + + // Direct $refs should not have aliases (only deduplicated inline schemas get aliases) + final CodegenProperty mobileProperty = personModel.vars.stream() + .filter(v -> v.baseName.equals("mobile")) + .findFirst() + .orElse(null); + Assert.assertNotNull(mobileProperty); + Assert.assertNull(mobileProperty.dataTypeAlias); + Assert.assertEquals(mobileProperty.dataType, "PhoneNumber"); + + final CodegenProperty homeProperty = personModel.vars.stream() + .filter(v -> v.baseName.equals("home")) + .findFirst() + .orElse(null); + Assert.assertNotNull(homeProperty); + Assert.assertNull(homeProperty.dataTypeAlias); + Assert.assertEquals(homeProperty.dataType, "PhoneNumber"); + } + + @Test(description = "test type aliases for deduplicated inline schemas") + public void typeAliasForDeduplicatedInlineSchemasTest() { + final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/inline-deduplicated-schemas.yaml"); + final GoClientCodegen codegen = new GoClientCodegen(); + codegen.setOpenAPI(openAPI); + + Schema demoResponseSchema = openAPI.getComponents().getSchemas().get("DemoResponse"); + CodegenModel demoModel = codegen.fromModel("DemoResponse", demoResponseSchema); + + // Call postProcessModels to trigger Go-specific dataType/dataTypeAlias swapping + ModelsMap modelsMap = new ModelsMap(); + ModelMap modelMap = new ModelMap(); + modelMap.setModel(demoModel); + modelsMap.setModels(Arrays.asList(modelMap)); + modelsMap.setImports(new ArrayList<>()); + modelsMap = codegen.postProcessModels(modelsMap); + demoModel = modelsMap.getModels().get(0).getModel(); + + Assert.assertEquals(demoModel.name, "DemoResponse"); + + // inlinePhone1 is the original (not deduplicated) + final CodegenProperty inlinePhone1 = demoModel.vars.stream() + .filter(v -> v.baseName.equals("inlinePhone1")) + .findFirst() + .orElse(null); + Assert.assertNotNull(inlinePhone1); + Assert.assertNull(inlinePhone1.dataTypeAlias); + Assert.assertEquals(inlinePhone1.dataType, "DemoResponseInlinePhone1"); + + // inlinePhone2 is deduplicated with inlinePhone1 + final CodegenProperty inlinePhone2 = demoModel.vars.stream() + .filter(v -> v.baseName.equals("inlinePhone2")) + .findFirst() + .orElse(null); + Assert.assertNotNull(inlinePhone2); + Assert.assertNotNull(inlinePhone2.dataTypeAlias); + Assert.assertEquals(inlinePhone2.dataType, "DemoResponseInlinePhone2"); + Assert.assertEquals(inlinePhone2.dataTypeAlias, "DemoResponseInlinePhone1"); + + // nestedPhones array items are deduplicated + final CodegenProperty nestedPhones = demoModel.vars.stream() + .filter(v -> v.baseName.equals("nestedPhones")) + .findFirst() + .orElse(null); + Assert.assertNotNull(nestedPhones); + Assert.assertTrue(nestedPhones.isContainer); + Assert.assertNotNull(nestedPhones.items); + Assert.assertNotNull(nestedPhones.items.dataTypeAlias); + Assert.assertEquals(nestedPhones.items.dataType, "DemoResponseNestedPhonesInner"); + Assert.assertEquals(nestedPhones.items.dataTypeAlias, "DemoResponseInlinePhone1"); + Assert.assertEquals(nestedPhones.dataType, "[]DemoResponseNestedPhonesInner"); + + // phoneHistory array items are deduplicated + final CodegenProperty phoneHistory = demoModel.vars.stream() + .filter(v -> v.baseName.equals("phoneHistory")) + .findFirst() + .orElse(null); + Assert.assertNotNull(phoneHistory); + Assert.assertTrue(phoneHistory.isContainer); + Assert.assertNotNull(phoneHistory.items); + Assert.assertNotNull(phoneHistory.items.dataTypeAlias); + Assert.assertEquals(phoneHistory.items.dataType, "DemoResponsePhoneHistoryInner"); + Assert.assertEquals(phoneHistory.items.dataTypeAlias, "DemoResponseInlinePhone1"); + Assert.assertEquals(phoneHistory.dataType, "[]DemoResponsePhoneHistoryInner"); + + // optionalNumber has different structure (no required fields) + final CodegenProperty optionalNumber = demoModel.vars.stream() + .filter(v -> v.baseName.equals("optionalNumber")) + .findFirst() + .orElse(null); + Assert.assertNotNull(optionalNumber); + Assert.assertNull(optionalNumber.dataTypeAlias); + Assert.assertEquals(optionalNumber.dataType, "DemoResponseOptionalNumber"); + + // requiredNumber is deduplicated with inlinePhone1 + final CodegenProperty requiredNumber = demoModel.vars.stream() + .filter(v -> v.baseName.equals("requiredNumber")) + .findFirst() + .orElse(null); + Assert.assertNotNull(requiredNumber); + Assert.assertNotNull(requiredNumber.dataTypeAlias); + Assert.assertEquals(requiredNumber.dataType, "DemoResponseRequiredNumber"); + Assert.assertEquals(requiredNumber.dataTypeAlias, "DemoResponseInlinePhone1"); + + // transactOptions array items are deduplicated (MapSchema with additionalProperties: true) + final CodegenProperty transactOptions = demoModel.vars.stream() + .filter(v -> v.baseName.equals("transactOptions")) + .findFirst() + .orElse(null); + Assert.assertNotNull(transactOptions); + Assert.assertTrue(transactOptions.isContainer); + Assert.assertNotNull(transactOptions.items); + // transactOptions is the ORIGINAL, so items should NOT have dataTypeAlias + Assert.assertNull(transactOptions.items.dataTypeAlias); + Assert.assertEquals(transactOptions.items.dataType, "DemoResponseTransactOptionsInner"); + Assert.assertEquals(transactOptions.dataType, "[]DemoResponseTransactOptionsInner"); + + // otherOptions array items are DEDUPLICATED with transactOptions (MapSchema with additionalProperties: true) + final CodegenProperty otherOptions = demoModel.vars.stream() + .filter(v -> v.baseName.equals("otherOptions")) + .findFirst() + .orElse(null); + Assert.assertNotNull(otherOptions); + Assert.assertTrue(otherOptions.isContainer); + Assert.assertNotNull(otherOptions.items); + Assert.assertNotNull(otherOptions.items.dataTypeAlias); + Assert.assertEquals(otherOptions.items.dataType, "DemoResponseOtherOptionsInner"); + Assert.assertEquals(otherOptions.items.dataTypeAlias, "DemoResponseTransactOptionsInner"); + Assert.assertEquals(otherOptions.dataType, "[]DemoResponseOtherOptionsInner"); + + // directMapObject is DEDUPLICATED with transactOptions items (MapSchema with additionalProperties: true) + final CodegenProperty directMapObject = demoModel.vars.stream() + .filter(v -> v.baseName.equals("directMapObject")) + .findFirst() + .orElse(null); + Assert.assertNotNull(directMapObject); + Assert.assertNotNull(directMapObject.dataTypeAlias); + Assert.assertEquals(directMapObject.dataType, "DemoResponseDirectMapObject"); + Assert.assertEquals(directMapObject.dataTypeAlias, "DemoResponseTransactOptionsInner"); + } } diff --git a/modules/openapi-generator/src/test/resources/3_0/inline-deduplicated-schemas.yaml b/modules/openapi-generator/src/test/resources/3_0/inline-deduplicated-schemas.yaml new file mode 100644 index 000000000000..533d412bf46c --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/inline-deduplicated-schemas.yaml @@ -0,0 +1,144 @@ +openapi: 3.0.0 +info: + title: OpenAPI Deduplication Demo + version: 1.0.0 + description: > + This spec demonstrates OpenAPI Generator schema deduplication and type alias generation in Go: + inline objects (direct properties), nested inline objects (arrays), and objects with different property requirements. + +paths: + /demo: + get: + summary: Demo endpoint + operationId: getDemo + responses: + '200': + description: Demo response + content: + application/json: + schema: + $ref: '#/components/schemas/DemoResponse' + +components: + schemas: + PhoneNumber: + type: object + required: + - number + properties: + countryCode: + type: string + number: + type: string + + FaxNumber: + type: object + required: + - number + properties: + countryCode: + type: string + number: + type: string + + DemoResponse: + type: object + properties: + inlinePhone1: + type: object + required: + - number + properties: + countryCode: + type: string + number: + type: string + inlinePhone2: + type: object + required: + - number + properties: + countryCode: + type: string + number: + type: string + + nestedPhones: + type: array + items: + type: object + required: + - number + properties: + countryCode: + type: string + number: + type: string + + phoneHistory: + type: array + items: + type: object + required: + - number + properties: + countryCode: + type: string + number: + type: string + + optionalNumber: + type: object + properties: + countryCode: + type: string + number: + type: string + requiredNumber: + type: object + required: + - number + properties: + countryCode: + type: string + number: + type: string + + # Test cases for additionalProperties: true (MapSchema) + # These should also generate type aliases even though isModel=false + transactOptions: + type: array + items: + type: object + additionalProperties: true + properties: + code: + type: string + description: + type: string + name: + type: string + + otherOptions: + type: array + items: + type: object + additionalProperties: true + properties: + code: + type: string + description: + type: string + name: + type: string + + directMapObject: + type: object + additionalProperties: true + properties: + code: + type: string + description: + type: string + name: + type: string From af9a898ac8805fafe882e5782baf75857e349927 Mon Sep 17 00:00:00 2001 From: Arieh Schneier <15041913+AriehSchneier@users.noreply.github.com> Date: Wed, 14 Jan 2026 15:17:40 +1100 Subject: [PATCH 2/4] Add `dataTypeAlias` to equals/hashCode/toString --- .../main/java/org/openapitools/codegen/CodegenProperty.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenProperty.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenProperty.java index 09eddfc86ad2..8ae213842acf 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenProperty.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenProperty.java @@ -986,6 +986,7 @@ public String toString() { sb.append(", setter='").append(setter).append('\''); sb.append(", description='").append(description).append('\''); sb.append(", dataType='").append(dataType).append('\''); + sb.append(", dataTypeAlias='").append(dataTypeAlias).append('\''); sb.append(", datatypeWithEnum='").append(datatypeWithEnum).append('\''); sb.append(", dataFormat='").append(dataFormat).append('\''); sb.append(", name='").append(name).append('\''); @@ -1173,6 +1174,7 @@ public boolean equals(Object o) { Objects.equals(setter, that.setter) && Objects.equals(description, that.description) && Objects.equals(dataType, that.dataType) && + Objects.equals(dataTypeAlias, that.dataTypeAlias) && Objects.equals(datatypeWithEnum, that.datatypeWithEnum) && Objects.equals(dataFormat, that.dataFormat) && Objects.equals(name, that.name) && @@ -1217,7 +1219,7 @@ public boolean equals(Object o) { public int hashCode() { return Objects.hash(openApiType, baseName, complexType, getter, setter, description, - dataType, datatypeWithEnum, dataFormat, name, min, max, defaultValue, + dataType, dataTypeAlias, datatypeWithEnum, dataFormat, name, min, max, defaultValue, defaultValueWithParam, baseType, containerType, containerTypeMapped, title, unescapedDescription, maxLength, minLength, pattern, example, jsonSchema, minimum, maximum, exclusiveMinimum, exclusiveMaximum, required, deprecated, From ed15fee7e9bad5c68d213e096ed61fa2168398c3 Mon Sep 17 00:00:00 2001 From: Arieh Schneier <15041913+AriehSchneier@users.noreply.github.com> Date: Thu, 15 Jan 2026 01:41:28 +1100 Subject: [PATCH 3/4] Claude added some extra code to resolve the code review --- .../codegen/InlineModelResolver.java | 8 ++++ .../codegen/InlineModelResolverTest.java | 47 +++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/InlineModelResolver.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/InlineModelResolver.java index 1fdb94f49bf7..5a83b6ba1e96 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/InlineModelResolver.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/InlineModelResolver.java @@ -667,6 +667,7 @@ private void flattenComposedChildren(String key, List children, boolean listIterator.set(schema); } else { Schema schema = new Schema().$ref(existing); + schema.addExtension("x-alias-name", innerModelName); schema.setRequired(component.getRequired()); listIterator.set(schema); } @@ -826,6 +827,7 @@ private void flattenProperties(OpenAPI openAPI, Map properties, String existing = matchGenerated(model); if (existing != null) { Schema schema = new Schema().$ref(existing); + schema.addExtension("x-alias-name", modelName); schema.setRequired(op.getRequired()); propsToUpdate.put(key, schema); } else { @@ -846,6 +848,7 @@ private void flattenProperties(OpenAPI openAPI, Map properties, String existing = matchGenerated(innerModel); if (existing != null) { Schema schema = new Schema().$ref(existing); + schema.addExtension("x-alias-name", modelName); schema.setRequired(op.getRequired()); property.setItems(schema); } else { @@ -876,6 +879,7 @@ private void flattenProperties(OpenAPI openAPI, Map properties, String existing = matchGenerated(innerModel); if (existing != null) { Schema schema = new Schema().$ref(existing); + schema.addExtension("x-alias-name", modelName); schema.setRequired(op.getRequired()); property.setAdditionalProperties(schema); } else { @@ -1037,6 +1041,10 @@ private void copyVendorExtensions(Schema source, Schema target) { return; } for (String extName : vendorExtensions.keySet()) { + // Don't overwrite x-alias-name that was set during deduplication + if ("x-alias-name".equals(extName) && target.getExtensions() != null && target.getExtensions().containsKey("x-alias-name")) { + continue; + } target.addExtension(extName, vendorExtensions.get(extName)); } } diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/InlineModelResolverTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/InlineModelResolverTest.java index 0670942f8fd0..56667b09f9f9 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/InlineModelResolverTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/InlineModelResolverTest.java @@ -1205,4 +1205,51 @@ public void doNotWrapSingleAllOfRefs() { assertNotNull(allOfRefWithDescriptionAndReadonly.getAllOf()); assertEquals(numberRangeRef, ((Schema) allOfRefWithDescriptionAndReadonly.getAllOf().get(0)).get$ref()); } + + @Test + public void testDeduplicationAddsAliasName() { + // Test that when inline schemas are deduplicated, the x-alias-name extension is set + OpenAPI openapi = new OpenAPI(); + openapi.setComponents(new Components()); + + // Create two models with identical inline schemas that will be deduplicated + openapi.getComponents().addSchemas("ModelA", new ObjectSchema() + .addProperty("name", new StringSchema()) + .addProperty("details", new ObjectSchema() + .addProperty("field1", new StringSchema()) + .addProperty("field2", new IntegerSchema()))); + + openapi.getComponents().addSchemas("ModelB", new ObjectSchema() + .addProperty("title", new StringSchema()) + .addProperty("info", new ObjectSchema() + .addProperty("field1", new StringSchema()) + .addProperty("field2", new IntegerSchema()))); + + new InlineModelResolver().flatten(openapi); + + // Check ModelA's property reference + Schema modelA = openapi.getComponents().getSchemas().get("ModelA"); + assertNotNull(modelA); + Schema detailsRef = (Schema) modelA.getProperties().get("details"); + assertNotNull(detailsRef); + assertNotNull(detailsRef.get$ref()); + assertEquals("#/components/schemas/ModelA_details", detailsRef.get$ref()); + + // Check ModelB's property reference - should be deduplicated to ModelA_details + Schema modelB = openapi.getComponents().getSchemas().get("ModelB"); + assertNotNull(modelB); + Schema infoRef = (Schema) modelB.getProperties().get("info"); + assertNotNull(infoRef); + assertNotNull(infoRef.get$ref()); + // The ref should point to the first schema created (ModelA_details) + assertEquals("#/components/schemas/ModelA_details", infoRef.get$ref()); + + // Verify x-alias-name extension is set on the deduplicated reference + assertNotNull(infoRef.getExtensions()); + assertTrue(infoRef.getExtensions().containsKey("x-alias-name")); + assertEquals("ModelB_info", infoRef.getExtensions().get("x-alias-name")); + + // Verify the first reference does not have x-alias-name (it's the original) + assertNull(detailsRef.getExtensions() != null ? detailsRef.getExtensions().get("x-alias-name") : null); + } } From 7089435234ee96cea2460fef6f37b2aa15a3fa7d Mon Sep 17 00:00:00 2001 From: Arieh Schneier <15041913+AriehSchneier@users.noreply.github.com> Date: Thu, 15 Jan 2026 01:59:04 +1100 Subject: [PATCH 4/4] Make sure to respect inlineSchemaNameMapping --- .../codegen/InlineModelResolver.java | 24 +++++++--- .../codegen/InlineModelResolverTest.java | 47 +++++++++++++++++++ 2 files changed, 65 insertions(+), 6 deletions(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/InlineModelResolver.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/InlineModelResolver.java index 5a83b6ba1e96..64f81ff3137d 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/InlineModelResolver.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/InlineModelResolver.java @@ -667,7 +667,7 @@ private void flattenComposedChildren(String key, List children, boolean listIterator.set(schema); } else { Schema schema = new Schema().$ref(existing); - schema.addExtension("x-alias-name", innerModelName); + schema.addExtension("x-alias-name", applyInlineSchemaNameMapping(innerModelName)); schema.setRequired(component.getRequired()); listIterator.set(schema); } @@ -827,7 +827,7 @@ private void flattenProperties(OpenAPI openAPI, Map properties, String existing = matchGenerated(model); if (existing != null) { Schema schema = new Schema().$ref(existing); - schema.addExtension("x-alias-name", modelName); + schema.addExtension("x-alias-name", applyInlineSchemaNameMapping(modelName)); schema.setRequired(op.getRequired()); propsToUpdate.put(key, schema); } else { @@ -848,7 +848,7 @@ private void flattenProperties(OpenAPI openAPI, Map properties, String existing = matchGenerated(innerModel); if (existing != null) { Schema schema = new Schema().$ref(existing); - schema.addExtension("x-alias-name", modelName); + schema.addExtension("x-alias-name", applyInlineSchemaNameMapping(modelName)); schema.setRequired(op.getRequired()); property.setItems(schema); } else { @@ -879,7 +879,7 @@ private void flattenProperties(OpenAPI openAPI, Map properties, String existing = matchGenerated(innerModel); if (existing != null) { Schema schema = new Schema().$ref(existing); - schema.addExtension("x-alias-name", modelName); + schema.addExtension("x-alias-name", applyInlineSchemaNameMapping(modelName)); schema.setRequired(op.getRequired()); property.setAdditionalProperties(schema); } else { @@ -989,6 +989,19 @@ private Schema modelFromProperty(OpenAPI openAPI, Schema object, String path) { return model; } + /** + * Apply inlineSchemaNameMapping if configured. + * + * @param name the inline schema name to map + * @return the mapped name if mapping exists, otherwise the original name + */ + private String applyInlineSchemaNameMapping(String name) { + if (inlineSchemaNameMapping.containsKey(name)) { + return inlineSchemaNameMapping.get(name); + } + return name; + } + /** * Move schema to components (if new) and return $ref to schema or * existing schema. @@ -1002,8 +1015,7 @@ private Schema makeSchemaInComponents(String name, Schema schema) { Schema refSchema; if (existing != null) { refSchema = new Schema().$ref(existing); - // Store the name this schema would have had if not deduplicated - refSchema.addExtension("x-alias-name", name); + refSchema.addExtension("x-alias-name", applyInlineSchemaNameMapping(name)); } else { if (resolveInlineEnums && schema.getEnum() != null && schema.getEnum().size() > 0) { LOGGER.warn("Model " + name + " promoted to its own schema due to resolveInlineEnums=true"); diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/InlineModelResolverTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/InlineModelResolverTest.java index 56667b09f9f9..cb4a112754ca 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/InlineModelResolverTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/InlineModelResolverTest.java @@ -1252,4 +1252,51 @@ public void testDeduplicationAddsAliasName() { // Verify the first reference does not have x-alias-name (it's the original) assertNull(detailsRef.getExtensions() != null ? detailsRef.getExtensions().get("x-alias-name") : null); } + + @Test + public void testDeduplicationWithInlineSchemaNameMapping() { + // Test that x-alias-name respects inlineSchemaNameMapping configuration + OpenAPI openapi = new OpenAPI(); + openapi.setComponents(new Components()); + + // Create two models with identical inline schemas that will be deduplicated + openapi.getComponents().addSchemas("ModelA", new ObjectSchema() + .addProperty("name", new StringSchema()) + .addProperty("details", new ObjectSchema() + .addProperty("field1", new StringSchema()) + .addProperty("field2", new IntegerSchema()))); + + openapi.getComponents().addSchemas("ModelB", new ObjectSchema() + .addProperty("title", new StringSchema()) + .addProperty("info", new ObjectSchema() + .addProperty("field1", new StringSchema()) + .addProperty("field2", new IntegerSchema()))); + + // Configure inlineSchemaNameMapping to rename ModelB_info to CustomInfoName + InlineModelResolver resolver = new InlineModelResolver(); + Map inlineSchemaNames = new HashMap<>(); + inlineSchemaNames.put("ModelB_info", "CustomInfoName"); + resolver.setInlineSchemaNameMapping(inlineSchemaNames); + resolver.flatten(openapi); + + // Check ModelB's property reference - should be deduplicated to ModelA_details + Schema modelB = openapi.getComponents().getSchemas().get("ModelB"); + assertNotNull(modelB); + Schema infoRef = (Schema) modelB.getProperties().get("info"); + assertNotNull(infoRef); + assertNotNull(infoRef.get$ref()); + // The ref should point to the first schema created (ModelA_details) + assertEquals("#/components/schemas/ModelA_details", infoRef.get$ref()); + + // Verify x-alias-name extension uses the MAPPED name, not the original name + assertNotNull(infoRef.getExtensions()); + assertTrue(infoRef.getExtensions().containsKey("x-alias-name")); + assertEquals("CustomInfoName", infoRef.getExtensions().get("x-alias-name")); + + // Verify CustomInfoName schema was NOT created (since it was deduplicated) + assertNull(openapi.getComponents().getSchemas().get("CustomInfoName")); + + // Verify ModelA_details schema was created (the deduplicated schema) + assertNotNull(openapi.getComponents().getSchemas().get("ModelA_details")); + } }