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 aea79c36dba9..c12c5a152180 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 @@ -3916,6 +3916,9 @@ protected String getCommonSchemaType(List schemas) { Schema first = schemas.get(0); try { + // a $ref to an alias of a primitive type has no model: use the aliased type, + // like the type of the property in the concrete models + first = unaliasSchema(first); if (StringUtils.isNotEmpty(first.get$ref())) { return toModelName(ModelUtils.getSimpleRef(first.get$ref())); } 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 ae92cae3a940..56269959817e 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 @@ -1385,6 +1385,34 @@ public void testOneOfAllOfInlineStringWithFormatDiscriminatorInheritance() { assertEquals("URI", catModel.discriminator.getPropertyType()); } + @Test + public void testOneOfDiscriminatorPropertyIsRefToPrimitiveAlias() { + final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/oneOf_issue_25226.yaml"); + new OpenAPINormalizer(openAPI, Map.of()).normalize(); + DefaultCodegen codegen = new DefaultCodegen(); + codegen.setUseOneOfInterfaces(true); + codegen.setOpenAPI(openAPI); + + // no model is generated for an alias of a primitive type: the aliased type is used, like for the subtypes + verifyOneOfDiscriminatorPropertyType(codegen, openAPI, "Shape", "String"); + verifyOneOfDiscriminatorPropertyType(codegen, openAPI, "Vehicle", "Integer"); + verifyOneOfDiscriminatorPropertyType(codegen, openAPI, "Document", "UUID"); + verifyOneOfDiscriminatorPropertyType(codegen, openAPI, "Fruit", "String"); + verifyOneOfDiscriminatorPropertyType(codegen, openAPI, "Animal", "String"); + // each subtype has its own alias + verifyOneOfDiscriminatorPropertyType(codegen, openAPI, "Tool", "String"); + // an enum, or an alias of an enum, is generated as a model + verifyOneOfDiscriminatorPropertyType(codegen, openAPI, "Pet", "BreedEnum"); + verifyOneOfDiscriminatorPropertyType(codegen, openAPI, "Toy", "ToyKind"); + } + + private void verifyOneOfDiscriminatorPropertyType(DefaultCodegen codegen, OpenAPI openAPI, String schemaName, + String expectedPropertyType) { + CodegenModel model = codegen.fromModel(schemaName, openAPI.getComponents().getSchemas().get(schemaName)); + assertTrue(model.getHasDiscriminatorWithNonEmptyMapping(), schemaName); + assertEquals(expectedPropertyType, model.discriminator.getPropertyType(), schemaName); + } + @Test public void testParentName() { final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/allOf.yaml"); diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java index d430b111df54..d513c1545cae 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java @@ -10249,6 +10249,40 @@ public void oneOfDiscriminatorType(String filename, boolean resolveInlineEnum, S .fileContains(expectedContains); } + @Test + public void issue_25226() throws IOException { + Map files = generateFromContract( + "src/test/resources/3_0/oneOf_issue_25226.yaml", SPRING_BOOT, + Map.of(USE_SPRING_BOOT4, true) + ); + + // no model is generated for an alias of a primitive type + assertThat(files).doesNotContainKeys("ShapeKind.java", "Wheels.java", "SchemaId.java", "Species.java", + "BreedAlias.java", "AnimalGroup.java"); + + // the getter of the oneOf interface uses the aliased type, like the subtypes + JavaFileAssert.assertThat(files.get("Shape.java")) + .fileContains("public String getKind();"); + JavaFileAssert.assertThat(files.get("Circle.java")) + .fileContains("public String getKind() {"); + JavaFileAssert.assertThat(files.get("Vehicle.java")) + .fileContains("public Integer getWheels();"); + JavaFileAssert.assertThat(files.get("Document.java")) + .fileContains("public UUID getSchemaId();"); + JavaFileAssert.assertThat(files.get("Fruit.java")) + .fileContains("public String getSpecies();"); + JavaFileAssert.assertThat(files.get("Animal.java")) + .fileContains("public String getGroup();"); + JavaFileAssert.assertThat(files.get("Tool.java")) + .fileContains("public String getCategory();"); + + // an enum, or an alias of an enum, is generated as a model + JavaFileAssert.assertThat(files.get("Pet.java")) + .fileContains("public BreedEnum getBreed();"); + JavaFileAssert.assertThat(files.get("Toy.java")) + .fileContains("public ToyKind getKind();"); + } + @Test void listItems_annotated_with_x_field_extra_annotation() throws IOException { Map files = generateFromContract("src/test/resources/3_0/issue_23705.yaml", SPRING_BOOT, diff --git a/modules/openapi-generator/src/test/resources/3_0/oneOf_issue_25226.yaml b/modules/openapi-generator/src/test/resources/3_0/oneOf_issue_25226.yaml new file mode 100644 index 000000000000..6b755a7910a2 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/oneOf_issue_25226.yaml @@ -0,0 +1,282 @@ +openapi: 3.0.3 +info: + title: oneOf discriminator property typed by a $ref to a primitive alias + description: > + The discriminator property of the oneOf subtypes is a $ref. When the referenced schema is an alias of + a primitive type (e.g. a plain `type: string`), no model is generated for it and the getter of the + generated oneOf interface must use the aliased type, like the implementing classes do. + version: 1.0.0 +paths: + /all: + get: + operationId: getAll + responses: + '200': + description: All the oneOf parents + content: + application/json: + schema: + $ref: '#/components/schemas/Holder' +components: + schemas: + Holder: + type: object + properties: + shape: + $ref: '#/components/schemas/Shape' + vehicle: + $ref: '#/components/schemas/Vehicle' + document: + $ref: '#/components/schemas/Document' + fruit: + $ref: '#/components/schemas/Fruit' + pet: + $ref: '#/components/schemas/Pet' + toy: + $ref: '#/components/schemas/Toy' + tool: + $ref: '#/components/schemas/Tool' + animal: + $ref: '#/components/schemas/Animal' + + # Both subtypes share the same alias of a string + Shape: + oneOf: + - $ref: '#/components/schemas/Circle' + - $ref: '#/components/schemas/Square' + discriminator: + propertyName: kind + mapping: + circle: '#/components/schemas/Circle' + square: '#/components/schemas/Square' + ShapeKind: + type: string + description: The kind of shape. + Circle: + type: object + required: + - kind + properties: + kind: + $ref: '#/components/schemas/ShapeKind' + radius: + type: number + Square: + type: object + required: + - kind + properties: + kind: + $ref: '#/components/schemas/ShapeKind' + side: + type: number + + # Alias of an integer + Vehicle: + oneOf: + - $ref: '#/components/schemas/Car' + - $ref: '#/components/schemas/Bike' + discriminator: + propertyName: wheels + mapping: + '4': '#/components/schemas/Car' + '2': '#/components/schemas/Bike' + Wheels: + type: integer + format: int32 + Car: + type: object + required: + - wheels + properties: + wheels: + $ref: '#/components/schemas/Wheels' + Bike: + type: object + required: + - wheels + properties: + wheels: + $ref: '#/components/schemas/Wheels' + + # Alias of a formatted string + Document: + oneOf: + - $ref: '#/components/schemas/Memo' + - $ref: '#/components/schemas/Note' + discriminator: + propertyName: schemaId + mapping: + memo: '#/components/schemas/Memo' + note: '#/components/schemas/Note' + SchemaId: + type: string + format: uuid + Memo: + type: object + required: + - schemaId + properties: + schemaId: + $ref: '#/components/schemas/SchemaId' + Note: + type: object + required: + - schemaId + properties: + schemaId: + $ref: '#/components/schemas/SchemaId' + + # Alias of an alias + Fruit: + oneOf: + - $ref: '#/components/schemas/Apple' + - $ref: '#/components/schemas/Pear' + discriminator: + propertyName: species + mapping: + apple: '#/components/schemas/Apple' + pear: '#/components/schemas/Pear' + SpeciesBase: + type: string + Species: + $ref: '#/components/schemas/SpeciesBase' + Apple: + type: object + required: + - species + properties: + species: + $ref: '#/components/schemas/Species' + Pear: + type: object + required: + - species + properties: + species: + $ref: '#/components/schemas/Species' + + # Alias of an enum: the enum model is generated, so it must be used + Pet: + oneOf: + - $ref: '#/components/schemas/Dog' + - $ref: '#/components/schemas/Cat' + discriminator: + propertyName: breed + mapping: + dog: '#/components/schemas/Dog' + cat: '#/components/schemas/Cat' + BreedEnum: + type: string + enum: + - dog + - cat + BreedAlias: + $ref: '#/components/schemas/BreedEnum' + Dog: + type: object + required: + - breed + properties: + breed: + $ref: '#/components/schemas/BreedAlias' + Cat: + type: object + required: + - breed + properties: + breed: + $ref: '#/components/schemas/BreedAlias' + + # $ref to an enum: the enum model is generated, so it must be used (unchanged behavior) + Toy: + oneOf: + - $ref: '#/components/schemas/Ball' + - $ref: '#/components/schemas/Doll' + discriminator: + propertyName: kind + mapping: + ball: '#/components/schemas/Ball' + doll: '#/components/schemas/Doll' + ToyKind: + type: string + enum: + - ball + - doll + Ball: + type: object + required: + - kind + properties: + kind: + $ref: '#/components/schemas/ToyKind' + Doll: + type: object + required: + - kind + properties: + kind: + $ref: '#/components/schemas/ToyKind' + + # Each subtype uses its own alias of a string + Tool: + oneOf: + - $ref: '#/components/schemas/Hammer' + - $ref: '#/components/schemas/Saw' + discriminator: + propertyName: category + mapping: + hammer: '#/components/schemas/Hammer' + saw: '#/components/schemas/Saw' + HammerCategory: + type: string + SawCategory: + type: string + Hammer: + type: object + required: + - category + properties: + category: + $ref: '#/components/schemas/HammerCategory' + Saw: + type: object + required: + - category + properties: + category: + $ref: '#/components/schemas/SawCategory' + + # The alias property is inherited from a shared base schema through allOf + Animal: + oneOf: + - $ref: '#/components/schemas/Bird' + - $ref: '#/components/schemas/Fish' + discriminator: + propertyName: group + mapping: + bird: '#/components/schemas/Bird' + fish: '#/components/schemas/Fish' + AnimalGroup: + type: string + BaseAnimal: + type: object + required: + - group + properties: + group: + $ref: '#/components/schemas/AnimalGroup' + Bird: + allOf: + - $ref: '#/components/schemas/BaseAnimal' + - type: object + properties: + wingspan: + type: number + Fish: + allOf: + - $ref: '#/components/schemas/BaseAnimal' + - type: object + properties: + length: + type: number