Skip to content

Commit 9658c05

Browse files
committed
When models are deduplicated, in go, create an alias type with the original name
1 parent 753330d commit 9658c05

7 files changed

Lines changed: 387 additions & 0 deletions

File tree

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,12 @@ public class CodegenProperty implements Cloneable, IJsonSchemaValidationProperti
187187
public boolean isDiscriminator;
188188
public boolean isNew; // true when this property overrides an inherited property
189189
public Boolean isOverridden; // true if the property is a parent property (not defined in child/current schema)
190+
/**
191+
* The type alias name to use when this property references a deduplicated inline model.
192+
* When non-null, code generators may emit a type alias declaration.
193+
*/
194+
@Getter @Setter
195+
public String dataTypeAlias;
190196
@Getter @Setter
191197
public List<String> _enum;
192198
@Getter @Setter

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4193,6 +4193,15 @@ public CodegenProperty fromProperty(String name, Schema p, boolean required, boo
41934193
String type = getSchemaType(p);
41944194
setNonArrayMapProperty(property, type);
41954195
property.isModel = (ModelUtils.isComposedSchema(referencedSchema) || ModelUtils.isObjectSchema(referencedSchema)) && ModelUtils.isModel(referencedSchema);
4196+
4197+
// Check if this property is reusing a model type that was generated/deduplicated by InlineModelResolver
4198+
// InlineModelResolver marks deduplicated schemas with x-alias-name vendor extension
4199+
if (p.get$ref() != null && original != null && original.getExtensions() != null) {
4200+
String dedupedName = (String) original.getExtensions().get("x-alias-name");
4201+
if (dedupedName != null && ModelUtils.isModel(referencedSchema)) {
4202+
property.dataTypeAlias = toModelName(dedupedName);
4203+
}
4204+
}
41964205
}
41974206

41984207
// restore original schema with default value, nullable, readonly etc

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -998,6 +998,8 @@ private Schema makeSchemaInComponents(String name, Schema schema) {
998998
Schema refSchema;
999999
if (existing != null) {
10001000
refSchema = new Schema().$ref(existing);
1001+
// Store the name this schema would have had if not deduplicated
1002+
refSchema.addExtension("x-alias-name", name);
10011003
} else {
10021004
if (resolveInlineEnums && schema.getEnum() != null && schema.getEnum().size() > 0) {
10031005
LOGGER.warn("Model " + name + " promoted to its own schema due to resolveInlineEnums=true");

modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractGoCodegen.java

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -736,6 +736,21 @@ public void postProcessModelProperty(CodegenModel model, CodegenProperty propert
736736
property.vendorExtensions.put("x-golang-is-container", true);
737737
}
738738
}
739+
740+
private void swapDataTypeAndAlias(CodegenProperty property, Map<String, String> typeAliasesMap) {
741+
if (property.dataTypeAlias != null) {
742+
String dedupedType = property.dataType;
743+
String aliasName = property.dataTypeAlias;
744+
// Swap: field uses the alias, alias definition points to deduplicated type
745+
property.dataType = aliasName;
746+
property.dataTypeAlias = dedupedType;
747+
748+
// Collect type alias (after swap, dataType is alias name, dataTypeAlias is target)
749+
if (!property.isContainer && !typeAliasesMap.containsKey(property.dataType)) {
750+
typeAliasesMap.put(property.dataType, property.dataTypeAlias);
751+
}
752+
}
753+
}
739754

740755
@Override
741756
public ModelsMap postProcessModels(ModelsMap objs) {
@@ -775,7 +790,25 @@ public ModelsMap postProcessModels(ModelsMap objs) {
775790
codegenProperties.addAll(inheritedProperties);
776791
}
777792

793+
// Collect reused model properties for type alias generation
794+
Map<String, String> typeAliasesMap = new LinkedHashMap<>();
795+
778796
for (CodegenProperty cp : codegenProperties) {
797+
// Swap dataType and dataTypeAlias so fields use the alias name
798+
swapDataTypeAndAlias(cp, typeAliasesMap);
799+
800+
// Also swap for array items and update the array's dataType
801+
if (cp.items != null && cp.items.dataTypeAlias != null) {
802+
String oldItemsDataType = cp.items.dataType;
803+
swapDataTypeAndAlias(cp.items, typeAliasesMap);
804+
String newItemsDataType = cp.items.dataType;
805+
806+
// Update the array's dataType to use the new items dataType
807+
if (cp.dataType != null && cp.dataType.contains(oldItemsDataType)) {
808+
cp.dataType = cp.dataType.replace(oldItemsDataType, newItemsDataType);
809+
}
810+
}
811+
779812
if (!addedTimeImport && ("time.Time".equals(cp.dataType) || (cp.items != null && "time.Time".equals(cp.items.complexType)))) {
780813
imports.add(createMapping("import", "time"));
781814
addedTimeImport = true;
@@ -855,6 +888,23 @@ public ModelsMap postProcessModels(ModelsMap objs) {
855888
if (generateUnmarshalJSON) {
856889
model.vendorExtensions.putIfAbsent("x-go-generate-unmarshal-json", true);
857890
}
891+
892+
// Convert type aliases map to list for template usage
893+
if (!typeAliasesMap.isEmpty()) {
894+
List<Map<String, String>> typeAliases = new ArrayList<>();
895+
for (Map.Entry<String, String> entry : typeAliasesMap.entrySet()) {
896+
if (!entry.getKey().equals(entry.getValue())) {
897+
Map<String, String> aliasMap = new HashMap<>();
898+
aliasMap.put("aliasName", entry.getKey());
899+
aliasMap.put("originalType", entry.getValue());
900+
typeAliases.add(aliasMap);
901+
}
902+
}
903+
if (!typeAliases.isEmpty()) {
904+
model.vendorExtensions.put("x-go-type-aliases", typeAliases);
905+
model.vendorExtensions.put("x-go-has-type-aliases", true);
906+
}
907+
}
858908
}
859909

860910
// recursively add import for mapping one type to multiple imports

modules/openapi-generator/src/main/resources/go/model_simple.mustache

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
{{#vendorExtensions.x-go-has-type-aliases}}
2+
// Type aliases for reused model types
3+
{{#vendorExtensions.x-go-type-aliases}}
4+
type {{aliasName}} = {{originalType}}
5+
{{/vendorExtensions.x-go-type-aliases}}
6+
7+
{{/vendorExtensions.x-go-has-type-aliases}}
18
// checks if the {{classname}} type satisfies the MappedNullable interface at compile time
29
var _ MappedNullable = &{{classname}}{}
310

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

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,18 @@
2424
import org.openapitools.codegen.CodegenModel;
2525
import org.openapitools.codegen.CodegenProperty;
2626
import org.openapitools.codegen.DefaultCodegen;
27+
import org.openapitools.codegen.InlineModelResolver;
2728
import org.openapitools.codegen.TestUtils;
2829
import org.openapitools.codegen.languages.GoClientCodegen;
30+
import org.openapitools.codegen.model.ModelMap;
31+
import org.openapitools.codegen.model.ModelsMap;
2932
import org.testng.Assert;
3033
import org.testng.annotations.DataProvider;
3134
import org.testng.annotations.Test;
3235

3336
import java.io.File;
37+
import java.util.ArrayList;
38+
import java.util.Arrays;
3439

3540
@SuppressWarnings("static-method")
3641
public class GoModelTest {
@@ -324,4 +329,168 @@ public void modelNameMappingsTest(String name, String expectedName, String expec
324329
Assert.assertEquals(cm.name, name);
325330
Assert.assertEquals(cm.classname, expectedName);
326331
}
332+
333+
@Test(description = "test that direct $ref usage does NOT create aliases")
334+
public void directRefNoAliasTest() {
335+
final Schema phoneNumberSchema = new Schema()
336+
.type("object")
337+
.addProperty("countryCode", new StringSchema())
338+
.addProperty("number", new StringSchema())
339+
.addRequiredItem("number");
340+
341+
final Schema personSchema = new Schema()
342+
.type("object")
343+
.addProperty("name", new StringSchema())
344+
.addProperty("mobile", new Schema().$ref("#/components/schemas/PhoneNumber"))
345+
.addProperty("home", new Schema().$ref("#/components/schemas/PhoneNumber"))
346+
.addRequiredItem("name");
347+
348+
final DefaultCodegen codegen = new GoClientCodegen();
349+
OpenAPI openAPI = TestUtils.createOpenAPI();
350+
openAPI.getComponents().addSchemas("PhoneNumber", phoneNumberSchema);
351+
openAPI.getComponents().addSchemas("Person", personSchema);
352+
codegen.setOpenAPI(openAPI);
353+
354+
final CodegenModel personModel = codegen.fromModel("Person", personSchema);
355+
Assert.assertEquals(personModel.name, "Person");
356+
Assert.assertEquals(personModel.vars.size(), 3);
357+
358+
// Direct $refs should not have aliases (only deduplicated inline schemas get aliases)
359+
final CodegenProperty mobileProperty = personModel.vars.stream()
360+
.filter(v -> v.baseName.equals("mobile"))
361+
.findFirst()
362+
.orElse(null);
363+
Assert.assertNotNull(mobileProperty);
364+
Assert.assertNull(mobileProperty.dataTypeAlias);
365+
Assert.assertEquals(mobileProperty.dataType, "PhoneNumber");
366+
367+
final CodegenProperty homeProperty = personModel.vars.stream()
368+
.filter(v -> v.baseName.equals("home"))
369+
.findFirst()
370+
.orElse(null);
371+
Assert.assertNotNull(homeProperty);
372+
Assert.assertNull(homeProperty.dataTypeAlias);
373+
Assert.assertEquals(homeProperty.dataType, "PhoneNumber");
374+
}
375+
376+
@Test(description = "test type aliases for deduplicated inline schemas")
377+
public void typeAliasForDeduplicatedInlineSchemasTest() {
378+
final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/inline-deduplicated-schemas.yaml");
379+
final GoClientCodegen codegen = new GoClientCodegen();
380+
codegen.setOpenAPI(openAPI);
381+
382+
Schema demoResponseSchema = openAPI.getComponents().getSchemas().get("DemoResponse");
383+
CodegenModel demoModel = codegen.fromModel("DemoResponse", demoResponseSchema);
384+
385+
// Call postProcessModels to trigger Go-specific dataType/dataTypeAlias swapping
386+
ModelsMap modelsMap = new ModelsMap();
387+
ModelMap modelMap = new ModelMap();
388+
modelMap.setModel(demoModel);
389+
modelsMap.setModels(Arrays.asList(modelMap));
390+
modelsMap.setImports(new ArrayList<>());
391+
modelsMap = codegen.postProcessModels(modelsMap);
392+
demoModel = modelsMap.getModels().get(0).getModel();
393+
394+
Assert.assertEquals(demoModel.name, "DemoResponse");
395+
396+
// inlinePhone1 is the original (not deduplicated)
397+
final CodegenProperty inlinePhone1 = demoModel.vars.stream()
398+
.filter(v -> v.baseName.equals("inlinePhone1"))
399+
.findFirst()
400+
.orElse(null);
401+
Assert.assertNotNull(inlinePhone1);
402+
Assert.assertNull(inlinePhone1.dataTypeAlias);
403+
Assert.assertEquals(inlinePhone1.dataType, "DemoResponseInlinePhone1");
404+
405+
// inlinePhone2 is deduplicated with inlinePhone1
406+
final CodegenProperty inlinePhone2 = demoModel.vars.stream()
407+
.filter(v -> v.baseName.equals("inlinePhone2"))
408+
.findFirst()
409+
.orElse(null);
410+
Assert.assertNotNull(inlinePhone2);
411+
Assert.assertNotNull(inlinePhone2.dataTypeAlias);
412+
Assert.assertEquals(inlinePhone2.dataType, "DemoResponseInlinePhone2");
413+
Assert.assertEquals(inlinePhone2.dataTypeAlias, "DemoResponseInlinePhone1");
414+
415+
// nestedPhones array items are deduplicated
416+
final CodegenProperty nestedPhones = demoModel.vars.stream()
417+
.filter(v -> v.baseName.equals("nestedPhones"))
418+
.findFirst()
419+
.orElse(null);
420+
Assert.assertNotNull(nestedPhones);
421+
Assert.assertTrue(nestedPhones.isContainer);
422+
Assert.assertNotNull(nestedPhones.items);
423+
Assert.assertNotNull(nestedPhones.items.dataTypeAlias);
424+
Assert.assertEquals(nestedPhones.items.dataType, "DemoResponseNestedPhonesInner");
425+
Assert.assertEquals(nestedPhones.items.dataTypeAlias, "DemoResponseInlinePhone1");
426+
Assert.assertEquals(nestedPhones.dataType, "[]DemoResponseNestedPhonesInner");
427+
428+
// phoneHistory array items are deduplicated
429+
final CodegenProperty phoneHistory = demoModel.vars.stream()
430+
.filter(v -> v.baseName.equals("phoneHistory"))
431+
.findFirst()
432+
.orElse(null);
433+
Assert.assertNotNull(phoneHistory);
434+
Assert.assertTrue(phoneHistory.isContainer);
435+
Assert.assertNotNull(phoneHistory.items);
436+
Assert.assertNotNull(phoneHistory.items.dataTypeAlias);
437+
Assert.assertEquals(phoneHistory.items.dataType, "DemoResponsePhoneHistoryInner");
438+
Assert.assertEquals(phoneHistory.items.dataTypeAlias, "DemoResponseInlinePhone1");
439+
Assert.assertEquals(phoneHistory.dataType, "[]DemoResponsePhoneHistoryInner");
440+
441+
// optionalNumber has different structure (no required fields)
442+
final CodegenProperty optionalNumber = demoModel.vars.stream()
443+
.filter(v -> v.baseName.equals("optionalNumber"))
444+
.findFirst()
445+
.orElse(null);
446+
Assert.assertNotNull(optionalNumber);
447+
Assert.assertNull(optionalNumber.dataTypeAlias);
448+
Assert.assertEquals(optionalNumber.dataType, "DemoResponseOptionalNumber");
449+
450+
// requiredNumber is deduplicated with inlinePhone1
451+
final CodegenProperty requiredNumber = demoModel.vars.stream()
452+
.filter(v -> v.baseName.equals("requiredNumber"))
453+
.findFirst()
454+
.orElse(null);
455+
Assert.assertNotNull(requiredNumber);
456+
Assert.assertNotNull(requiredNumber.dataTypeAlias);
457+
Assert.assertEquals(requiredNumber.dataType, "DemoResponseRequiredNumber");
458+
Assert.assertEquals(requiredNumber.dataTypeAlias, "DemoResponseInlinePhone1");
459+
460+
// transactOptions array items are deduplicated (MapSchema with additionalProperties: true)
461+
final CodegenProperty transactOptions = demoModel.vars.stream()
462+
.filter(v -> v.baseName.equals("transactOptions"))
463+
.findFirst()
464+
.orElse(null);
465+
Assert.assertNotNull(transactOptions);
466+
Assert.assertTrue(transactOptions.isContainer);
467+
Assert.assertNotNull(transactOptions.items);
468+
// transactOptions is the ORIGINAL, so items should NOT have dataTypeAlias
469+
Assert.assertNull(transactOptions.items.dataTypeAlias);
470+
Assert.assertEquals(transactOptions.items.dataType, "DemoResponseTransactOptionsInner");
471+
Assert.assertEquals(transactOptions.dataType, "[]DemoResponseTransactOptionsInner");
472+
473+
// otherOptions array items are DEDUPLICATED with transactOptions (MapSchema with additionalProperties: true)
474+
final CodegenProperty otherOptions = demoModel.vars.stream()
475+
.filter(v -> v.baseName.equals("otherOptions"))
476+
.findFirst()
477+
.orElse(null);
478+
Assert.assertNotNull(otherOptions);
479+
Assert.assertTrue(otherOptions.isContainer);
480+
Assert.assertNotNull(otherOptions.items);
481+
Assert.assertNotNull(otherOptions.items.dataTypeAlias);
482+
Assert.assertEquals(otherOptions.items.dataType, "DemoResponseOtherOptionsInner");
483+
Assert.assertEquals(otherOptions.items.dataTypeAlias, "DemoResponseTransactOptionsInner");
484+
Assert.assertEquals(otherOptions.dataType, "[]DemoResponseOtherOptionsInner");
485+
486+
// directMapObject is DEDUPLICATED with transactOptions items (MapSchema with additionalProperties: true)
487+
final CodegenProperty directMapObject = demoModel.vars.stream()
488+
.filter(v -> v.baseName.equals("directMapObject"))
489+
.findFirst()
490+
.orElse(null);
491+
Assert.assertNotNull(directMapObject);
492+
Assert.assertNotNull(directMapObject.dataTypeAlias);
493+
Assert.assertEquals(directMapObject.dataType, "DemoResponseDirectMapObject");
494+
Assert.assertEquals(directMapObject.dataTypeAlias, "DemoResponseTransactOptionsInner");
495+
}
327496
}

0 commit comments

Comments
 (0)