From 0d4262018a5b2658089c2d19d9bda11dbdd184a9 Mon Sep 17 00:00:00 2001 From: cmuchinsky <23175991+cmuchinsky@users.noreply.github.com> Date: Mon, 12 Oct 2026 08:41:23 -0400 Subject: [PATCH] [core] Resolve primitive aliases for the oneOf discriminator property type When the discriminator property of all the mapped subtypes of a oneOf is a $ref to the same alias of a primitive type (e.g. a plain `type: string` schema), DefaultCodegen#getCommonSchemaType returned the name of the alias schema as the type of the discriminator getter of the oneOf interface. No model is generated for such an alias, so the generated interface did not compile (cannot find symbol), while the subtypes use the aliased type (e.g. String). Unalias the schema first, like getSingleSchemaType does for the property type of the subtypes. A $ref that is still present afterwards (enum, object, composed schema, schema mapping) keeps being used as a model name. Regression since 7.26.0 (#24812). fix #25226 Co-Authored-By: Claude Sonnet 5.5 --- .../openapitools/codegen/DefaultCodegen.java | 3 + .../codegen/DefaultCodegenTest.java | 28 ++ .../java/spring/SpringCodegenTest.java | 34 +++ .../test/resources/3_0/oneOf_issue_25226.yaml | 282 ++++++++++++++++++ 4 files changed, 347 insertions(+) create mode 100644 modules/openapi-generator/src/test/resources/3_0/oneOf_issue_25226.yaml 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