diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/DiscriminatorUtils.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/DiscriminatorUtils.java index 546c201dae5b..dedb47ac1184 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/DiscriminatorUtils.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/DiscriminatorUtils.java @@ -136,10 +136,17 @@ public static DiscriminatorData recursiveGetDiscriminator( DiscriminatorData foundDisc = new DiscriminatorData(refSchema.getDiscriminator(), null); if (foundDisc.getDiscriminator() != null) { String discriminatorPropertyName = foundDisc.getDiscriminator().getPropertyName(); - return new DiscriminatorData( - foundDisc.getDiscriminator(), - DiscriminatorUtils.getDiscriminatorSchema(refSchema, discriminatorPropertyName) - ); + Schema discriminatorSchema = DiscriminatorUtils.getDiscriminatorSchema(refSchema, discriminatorPropertyName); + if (discriminatorSchema == null) { + // The schema declares a discriminator but does not itself define the discriminator + // property (e.g. a oneOf/anyOf interface whose property is inherited by each + // alternative from a shared base). Descend into the alternatives to discover the + // property type so the generated getter matches the concrete classes rather than + // falling back to String. Ref: issue #22636. + discriminatorSchema = findDiscriminatorSchemaInAlternatives( + openAPI, refSchema, discriminatorPropertyName, new ArrayList<>()); + } + return new DiscriminatorData(foundDisc.getDiscriminator(), discriminatorSchema); } if (legacyDiscriminatorBehavior) { @@ -199,6 +206,54 @@ private static Schema getDiscriminatorSchema(Schema schema, String discriminator return discSchema; } + /** + * Recursively look for the schema of the discriminator property within the alternatives (oneOf, + * anyOf, allOf) of a composed schema. This is used when a schema declares a discriminator but + * does not itself define the discriminator property, so its type must be discovered from the + * schemas that do (typically a shared base inherited via allOf by each alternative). + * + * @param openAPI the openAPI specification + * @param schema the schema whose alternatives should be searched + * @param discriminatorName the name of the discriminator property + * @param visitedSchemas the schemas already visited, to guard against recursive definitions + * @return the discriminator property schema if found, otherwise null + */ + private static Schema findDiscriminatorSchemaInAlternatives(OpenAPI openAPI, + Schema schema, + String discriminatorName, + List visitedSchemas) { + Schema refSchema = ModelUtils.getReferencedSchema(openAPI, schema); + for (Schema s : visitedSchemas) { + if (s == refSchema) { + return null; + } + } + visitedSchemas.add(refSchema); + + Schema discSchema = getDiscriminatorSchema(refSchema, discriminatorName); + if (discSchema != null) { + return discSchema; + } + + List alternatives = new ArrayList<>(); + if (refSchema.getOneOf() != null) { + alternatives.addAll(refSchema.getOneOf()); + } + if (refSchema.getAnyOf() != null) { + alternatives.addAll(refSchema.getAnyOf()); + } + if (refSchema.getAllOf() != null) { + alternatives.addAll(refSchema.getAllOf()); + } + for (Schema alternative : alternatives) { + Schema found = findDiscriminatorSchemaInAlternatives(openAPI, alternative, discriminatorName, visitedSchemas); + if (found != null) { + return found; + } + } + return null; + } + /** * Check whether the alternative schemas share a discriminator, and if they do, return it * 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 7e12b85fd108..89ea88a70775 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 @@ -1291,6 +1291,26 @@ public void testOneOfAllOfInlineStringWithFormatDiscriminatorInheritance() { assertEquals("URI", catModel.discriminator.getPropertyType()); } + @Test + public void testOneOfDiscriminatorOnInterfacePropertyInAlternatives() { + // The oneOf interface declares the discriminator on itself but does not define the + // discriminator property locally; each alternative inherits it (an enum ref) from a shared + // base via allOf. The interface getter must resolve to the enum type, not String. + // Ref: issue #22636. + final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/oneOfDiscriminator.yaml"); + new OpenAPINormalizer(openAPI, Map.of()).normalize(); + DefaultCodegen codegen = new DefaultCodegen(); + codegen.setUseOneOfInterfaces(true); + codegen.setLegacyDiscriminatorBehavior(false); + codegen.setOpenAPI(openAPI); + + Schema inner = openAPI.getComponents().getSchemas().get("PetRequestV3"); + CodegenModel innerModel = codegen.fromModel("PetRequestV3", inner); + assertTrue(innerModel.getHasDiscriminatorWithNonEmptyMapping()); + assertTrue(innerModel.discriminator.getIsEnum()); + assertEquals("PetType", innerModel.discriminator.getPropertyType()); + } + @Test public void testParentName() { final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/allOf.yaml"); diff --git a/modules/openapi-generator/src/test/resources/3_0/oneOfDiscriminator.yaml b/modules/openapi-generator/src/test/resources/3_0/oneOfDiscriminator.yaml index 03aefd590817..4414a0c4f0cf 100644 --- a/modules/openapi-generator/src/test/resources/3_0/oneOfDiscriminator.yaml +++ b/modules/openapi-generator/src/test/resources/3_0/oneOfDiscriminator.yaml @@ -384,4 +384,46 @@ components: Bike: type: object allOf: - - $ref: '#/components/schemas/Vehicle' \ No newline at end of file + - $ref: '#/components/schemas/Vehicle' + # oneOf interface that declares the discriminator on the interface schema ITSELF (not on the + # alternatives) and does not define the discriminator property locally. The property type is + # an enum ref inherited by each alternative from a shared base (PetV3Base) via allOf. The + # interface getter must use the enum type (PetType), not String. Ref: issue #22636. + PetRequestV3: + type: object + oneOf: + - $ref: '#/components/schemas/CatRequestV3' + - $ref: '#/components/schemas/DogRequestV3' + discriminator: + propertyName: petType + mapping: + CAT: '#/components/schemas/CatRequestV3' + DOG: '#/components/schemas/DogRequestV3' + PetType: + type: string + enum: + - CAT + - DOG + PetV3Base: + type: object + required: + - petType + properties: + petType: + $ref: '#/components/schemas/PetType' + CatRequestV3: + type: object + allOf: + - $ref: '#/components/schemas/PetV3Base' + - type: object + properties: + indoor: + type: boolean + DogRequestV3: + type: object + allOf: + - $ref: '#/components/schemas/PetV3Base' + - type: object + properties: + trained: + type: boolean \ No newline at end of file