From a304e004419f4edb60d977ee88a70a991c7a0700 Mon Sep 17 00:00:00 2001 From: Ignacio Vidal Date: Tue, 21 Jul 2026 20:05:38 +0100 Subject: [PATCH] [core] Resolve oneOf discriminator type when property is inherited by alternatives When a oneOf/anyOf interface declares the discriminator on itself but does not define the discriminator property locally, the property type was resolved as String instead of the actual type. This happens when each alternative inherits the discriminator property from a shared base via allOf, while the discriminator keyword lives on the oneOf interface schema rather than on the base. recursiveGetDiscriminator now descends into the schema's oneOf/anyOf/allOf alternatives (cycle-guarded) to discover the discriminator property schema when the interface schema does not define it locally, so the generated interface getter uses the concrete type (e.g. an enum) rather than falling back to String. Adds a DefaultCodegenTest case and matching schemas in oneOfDiscriminator.yaml. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01Lw5PwrLknPHfsnkwTu2N1B --- .../codegen/utils/DiscriminatorUtils.java | 63 +++++++++++++++++-- .../codegen/DefaultCodegenTest.java | 20 ++++++ .../resources/3_0/oneOfDiscriminator.yaml | 44 ++++++++++++- 3 files changed, 122 insertions(+), 5 deletions(-) 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