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 006bdc827c08..db8b1b31efb6 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 @@ -3684,13 +3684,13 @@ protected Schema getDiscriminatorSchema(Schema schema, String discriminatorName) } /** - * Get the property type for the discriminator + * Get the property type for the discriminator from the schema, or its child schemas (from oneOf/anyOf refs) or returns String as a fallback * * @param schema The input OAS schema. * @param discriminatorPropertyName The name of the discriminator property. */ protected String getDiscriminatorPropertyType(Schema schema, String discriminatorPropertyName) { - return DiscriminatorUtils.getDiscriminatorPropertyType(schema, discriminatorPropertyName) + return DiscriminatorUtils.getDiscriminatorPropertyType(openAPI, schema, discriminatorPropertyName) .map(this::toModelName) .orElseGet(() -> typeMapping.get("string")); } 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 5ed6f5b0761c..7e3187d8e8fb 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 @@ -17,6 +17,8 @@ public class DiscriminatorUtils { private static final String CONFLICTING_DISCRIMINATOR_NAMES = "The alternative schemas have conflicting discriminator property names. The schemas must have the same property name, but found {}"; + private DiscriminatorUtils(){} + /** * Gets the simple ref name of the discriminator property type from the schema. * @@ -24,10 +26,11 @@ public class DiscriminatorUtils { * @param discriminatorPropertyName The name of the discriminator property. * @return referenced type name, or an empty optional if unavailable */ - public static Optional getDiscriminatorPropertyType(Schema schema, String discriminatorPropertyName) { + public static Optional getDiscriminatorPropertyType(OpenAPI openAPI, Schema schema, String discriminatorPropertyName) { return Optional.ofNullable(getDiscriminatorSchema(schema, discriminatorPropertyName)) .map(Schema::get$ref) - .map(ModelUtils::getSimpleRef); + .map(ModelUtils::getSimpleRef) + .or(()-> getDiscriminatorPropertyTypeFromChildren(openAPI, schema, discriminatorPropertyName)); } /** @@ -49,6 +52,74 @@ public static Schema getDiscriminatorSchema(Schema schema, String discriminatorN return discSchema; } + /** + * Resolve the discriminator property type by inspecting the oneOf/anyOf child schemas. Returns the simple + * ref name of the first child that declares the discriminator property as a $ref (e.g. an enum), or an + * empty optional if no child resolves to a typed property. + * + * @param openAPI the OpenAPI specification, used to resolve referenced child schemas. + * @param schema The oneOf/anyOf interface schema. + * @param discriminatorPropertyName The name of the discriminator property. + */ + static Optional getDiscriminatorPropertyTypeFromChildren(OpenAPI openAPI, Schema schema, String discriminatorPropertyName) { + List children = new ArrayList<>(); + if (schema.getOneOf() != null) { + children.addAll(schema.getOneOf()); + } + if (schema.getAnyOf() != null) { + children.addAll(schema.getAnyOf()); + } + for (Schema child : children) { + Schema resolved = ModelUtils.getReferencedSchema(openAPI, child); + if (resolved == null) { + continue; + } + Schema discSchema = getDiscriminatorSchemaDeep(openAPI, resolved, discriminatorPropertyName, new ArrayList<>()); + if (discSchema != null && discSchema.get$ref() != null) { + return Optional.ofNullable(ModelUtils.getSimpleRef(discSchema.get$ref())); + } + } + return Optional.empty(); + } + + /** + * Like {@link #getDiscriminatorSchema(Schema, String)}, but also chases the discriminator property through + * a schema's allOf members. A oneOf child commonly carries the discriminator property indirectly, via an + * allOf reference to a shared base schema rather than as a direct property. + * + * @param openAPI the OpenAPI specification, used to resolve referenced allOf members. + * @param schema The schema to inspect. + * @param discriminatorName The name of the discriminator property. + * @param visited A list of schemas already visited in the recursion, to avoid infinite loops. + * @return The discriminator property schema, or null if not found. + */ + static Schema getDiscriminatorSchemaDeep(OpenAPI openAPI, Schema schema, String discriminatorName, List visited) { + for (Schema s : visited) { + if (s == schema) { + return null; + } + } + visited.add(schema); + + Schema direct = getDiscriminatorSchema(schema, discriminatorName); + if (direct != null) { + return direct; + } + if (ModelUtils.isAllOf(schema)) { + for (Object member : schema.getAllOf()) { + Schema resolvedMember = ModelUtils.getReferencedSchema(openAPI, (Schema) member); + if (resolvedMember == null) { + continue; + } + Schema found = getDiscriminatorSchemaDeep(openAPI, resolvedMember, discriminatorName, visited); + if (found != null) { + return found; + } + } + } + return null; + } + /** * Recursively look in Schema sc for the discriminator and 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 f46f712a3890..6408bd71dc4b 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 @@ -1210,6 +1210,51 @@ public void testEnumDiscriminatorWithDescriptionOverridden3_1() { assertTrue(fruitModel.getVars().get(0).isEnumRef); } + @Test + public void testOneOfDiscriminatorTypeResolvedFromSharedBase() { + // The oneOf interface (PetRequest) declares no properties of its own; the discriminator + // property `petType` is declared only on the shared base (PetBase), which the children + // inherit via allOf. The discriminator property type must still resolve to the enum model + // (PetType) by inspecting the mapped child schemas, rather than falling back to "string". + final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/oneof_discriminator_enum_shared_base.yaml"); + DefaultCodegen codegen = new DefaultCodegen(); + codegen.setUseOneOfInterfaces(true); + codegen.setOpenAPI(openAPI); + + Schema petRequest = openAPI.getComponents().getSchemas().get("PetRequest"); + CodegenModel petRequestModel = codegen.fromModel("PetRequest", petRequest); + + assertTrue(petRequestModel.getHasDiscriminatorWithNonEmptyMapping()); + assertEquals("PetType", petRequestModel.discriminator.getPropertyType()); + + // The concrete subtypes resolve the same property type, so the generated getter signatures + // are consistent with the interface (no String-vs-enum return type clash). + Schema catRequest = openAPI.getComponents().getSchemas().get("CatRequest"); + CodegenModel catRequestModel = codegen.fromModel("CatRequest", catRequest); + CodegenProperty petTypeVar = catRequestModel.getVars().stream() + .filter(v -> "petType".equals(v.baseName)) + .findFirst() + .orElseThrow(() -> new AssertionError("petType property not found on CatRequest")); + assertEquals("PetType", petTypeVar.dataType); + } + + @Test + public void testOneOfDiscriminatorTypeFallsBackToStringWhenNotTyped() { + // The discriminator property (petType) is declared only as a plain inline string on the children, + // with no $ref to a typed schema. There is nothing to resolve from the schema or its children, so + // the discriminator property type must fall back to "string". + final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/oneof_discriminator_string_fallback.yaml"); + DefaultCodegen codegen = new DefaultCodegen(); + codegen.setUseOneOfInterfaces(true); + codegen.setOpenAPI(openAPI); + + Schema petRequest = openAPI.getComponents().getSchemas().get("PetRequest"); + CodegenModel petRequestModel = codegen.fromModel("PetRequest", petRequest); + + assertTrue(petRequestModel.getHasDiscriminatorWithNonEmptyMapping()); + assertEquals("String", petRequestModel.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/java/org/openapitools/codegen/utils/DiscriminatorUtilsTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/utils/DiscriminatorUtilsTest.java new file mode 100644 index 000000000000..20695c580502 --- /dev/null +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/utils/DiscriminatorUtilsTest.java @@ -0,0 +1,96 @@ +package org.openapitools.codegen.utils; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.media.Schema; +import org.openapitools.codegen.TestUtils; +import org.testng.annotations.Test; + +import java.util.Optional; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; + +public class DiscriminatorUtilsTest { + + /** + * A pure oneOf interface schema declares no properties of its own, so resolving the discriminator + * property type from the schema alone yields nothing. The type must instead be resolved from the + * mapped child schemas, which inherit the discriminator property from a shared base via allOf. + */ + @Test + public void resolvesDiscriminatorPropertyTypeFromChildrenWhenNotOnSchema() { + final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/oneof_discriminator_enum_shared_base.yaml"); + Schema petRequest = openAPI.getComponents().getSchemas().get("PetRequest"); + + // The oneOf interface declares no discriminator property of its own, so the type is resolved from + // the mapped children, finding the enum ref (PetType) declared on the shared base. + Optional resolved = DiscriminatorUtils.getDiscriminatorPropertyType(openAPI, petRequest, "petType"); + assertEquals(resolved.orElse(null), "PetType"); + } + + /** + * When the discriminator property is declared directly on the schema, the own-properties resolution is + * used and the children are not consulted. + */ + @Test + public void resolvesDiscriminatorPropertyTypeFromOwnPropertiesWhenPresent() { + final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/oneof_discriminator_enum_shared_base.yaml"); + Schema petBase = openAPI.getComponents().getSchemas().get("PetBase"); + + Optional resolved = DiscriminatorUtils.getDiscriminatorPropertyType(openAPI, petBase, "petType"); + assertEquals(resolved.orElse(null), "PetType"); + } + + /** + * The children fallback also resolves the discriminator property type when the interface uses anyOf + * rather than oneOf. + */ + @Test + public void resolvesDiscriminatorPropertyTypeFromAnyOfChildren() { + final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/anyof_discriminator_enum_shared_base.yaml"); + Schema petRequest = openAPI.getComponents().getSchemas().get("PetRequest"); + + Optional resolved = DiscriminatorUtils.getDiscriminatorPropertyType(openAPI, petRequest, "petType"); + assertEquals(resolved.orElse(null), "PetType"); + } + + /** + * The discriminator property is reached only by recursing through more than one level of allOf (the + * children allOf an intermediate base that itself allOf the grandparent declaring the property). + */ + @Test + public void resolvesDiscriminatorPropertyTypeThroughMultiLevelAllOf() { + final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/oneof_discriminator_enum_nested_base.yaml"); + Schema petRequest = openAPI.getComponents().getSchemas().get("PetRequest"); + + Optional resolved = DiscriminatorUtils.getDiscriminatorPropertyType(openAPI, petRequest, "petType"); + assertEquals(resolved.orElse(null), "PetType"); + } + + /** + * When neither the schema nor its children declare the discriminator property as a typed ($ref) schema + * - the children carry it only as a plain inline string - there is nothing to resolve, so the lookup + * returns empty (the caller then falls back to "string"). + */ + @Test + public void returnsEmptyWhenDiscriminatorPropertyHasNoTypedSchema() { + final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/oneof_discriminator_string_fallback.yaml"); + Schema petRequest = openAPI.getComponents().getSchemas().get("PetRequest"); + + assertFalse(DiscriminatorUtils.getDiscriminatorPropertyType(openAPI, petRequest, "petType").isPresent()); + } + + /** + * A cyclic allOf composition (two bases that allOf each other) must not cause the allOf descent to + * recurse infinitely. Resolution terminates; since no schema in the cycle declares the discriminator + * property as a typed $ref, the lookup returns empty. + */ + @Test + public void terminatesOnCyclicAllOfComposition() { + final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/oneof_discriminator_cyclic_allof.yaml"); + Schema petRequest = openAPI.getComponents().getSchemas().get("PetRequest"); + + // Must return (not StackOverflowError) - the visited-set guard breaks the allOf cycle. + assertFalse(DiscriminatorUtils.getDiscriminatorPropertyType(openAPI, petRequest, "petType").isPresent()); + } +} diff --git a/modules/openapi-generator/src/test/resources/3_0/anyof_discriminator_enum_shared_base.yaml b/modules/openapi-generator/src/test/resources/3_0/anyof_discriminator_enum_shared_base.yaml new file mode 100644 index 000000000000..0e353edced96 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/anyof_discriminator_enum_shared_base.yaml @@ -0,0 +1,70 @@ +openapi: 3.0.3 +info: + title: anyOf discriminator with shared base + description: > + Same shape as the oneOf shared-base case, but the interface schema uses anyOf + instead of oneOf. The discriminator property type must still be resolved from + the mapped child schemas (through their allOf base), exercising the anyOf + branch of the children fallback. + version: 1.0.0 +paths: + /pets: + post: + operationId: createPet + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PetRequest' + responses: + '201': + description: created +components: + schemas: + PetType: + type: string + enum: + - CAT + - DOG + description: Discriminator value identifying the type of pet + + PetRequest: + type: object + anyOf: + - $ref: '#/components/schemas/CatRequest' + - $ref: '#/components/schemas/DogRequest' + discriminator: + propertyName: petType + mapping: + CAT: '#/components/schemas/CatRequest' + DOG: '#/components/schemas/DogRequest' + + PetBase: + type: object + required: + - petType + - name + properties: + petType: + $ref: '#/components/schemas/PetType' + name: + type: string + + CatRequest: + type: object + allOf: + - $ref: '#/components/schemas/PetBase' + - type: object + properties: + indoor: + type: boolean + + DogRequest: + type: object + allOf: + - $ref: '#/components/schemas/PetBase' + - type: object + properties: + trained: + type: boolean diff --git a/modules/openapi-generator/src/test/resources/3_0/oneof_discriminator_cyclic_allof.yaml b/modules/openapi-generator/src/test/resources/3_0/oneof_discriminator_cyclic_allof.yaml new file mode 100644 index 000000000000..c5404baffe8c --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/oneof_discriminator_cyclic_allof.yaml @@ -0,0 +1,63 @@ +openapi: 3.0.3 +info: + title: oneOf discriminator with a cyclic allOf composition + description: > + The children allOf two bases (PetBaseA, PetBaseB) that allOf each other, forming + a cycle. Resolving the discriminator property type must terminate instead of + recursing infinitely; since no schema in the cycle declares the discriminator + property as a typed $ref, the type falls back to "string". + version: 1.0.0 +paths: + /pets: + post: + operationId: createPet + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PetRequest' + responses: + '201': + description: created +components: + schemas: + PetRequest: + type: object + oneOf: + - $ref: '#/components/schemas/CatRequest' + - $ref: '#/components/schemas/DogRequest' + discriminator: + propertyName: petType + mapping: + CAT: '#/components/schemas/CatRequest' + DOG: '#/components/schemas/DogRequest' + + # Two bases that allOf each other -> cyclic allOf composition. + PetBaseA: + type: object + allOf: + - $ref: '#/components/schemas/PetBaseB' + + PetBaseB: + type: object + allOf: + - $ref: '#/components/schemas/PetBaseA' + + CatRequest: + type: object + allOf: + - $ref: '#/components/schemas/PetBaseA' + - type: object + properties: + indoor: + type: boolean + + DogRequest: + type: object + allOf: + - $ref: '#/components/schemas/PetBaseB' + - type: object + properties: + trained: + type: boolean diff --git a/modules/openapi-generator/src/test/resources/3_0/oneof_discriminator_enum_nested_base.yaml b/modules/openapi-generator/src/test/resources/3_0/oneof_discriminator_enum_nested_base.yaml new file mode 100644 index 000000000000..9f059531fbdf --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/oneof_discriminator_enum_nested_base.yaml @@ -0,0 +1,81 @@ +openapi: 3.0.3 +info: + title: oneOf discriminator with multi-level allOf base + description: > + The discriminator property is declared on a grandparent schema (PetRoot). The + children allOf an intermediate base (PetBase) that itself allOf the grandparent, + so the discriminator property is reached only by recursing through more than one + level of allOf. Exercises the recursive descent of getDiscriminatorSchemaDeep. + version: 1.0.0 +paths: + /pets: + post: + operationId: createPet + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PetRequest' + responses: + '201': + description: created +components: + schemas: + PetType: + type: string + enum: + - CAT + - DOG + description: Discriminator value identifying the type of pet + + PetRequest: + type: object + oneOf: + - $ref: '#/components/schemas/CatRequest' + - $ref: '#/components/schemas/DogRequest' + discriminator: + propertyName: petType + mapping: + CAT: '#/components/schemas/CatRequest' + DOG: '#/components/schemas/DogRequest' + + # Grandparent: declares the discriminator property. + PetRoot: + type: object + required: + - petType + properties: + petType: + $ref: '#/components/schemas/PetType' + + # Intermediate base: inherits the discriminator property from the grandparent + # via allOf and adds a property of its own. + PetBase: + type: object + allOf: + - $ref: '#/components/schemas/PetRoot' + - type: object + required: + - name + properties: + name: + type: string + + CatRequest: + type: object + allOf: + - $ref: '#/components/schemas/PetBase' + - type: object + properties: + indoor: + type: boolean + + DogRequest: + type: object + allOf: + - $ref: '#/components/schemas/PetBase' + - type: object + properties: + trained: + type: boolean diff --git a/modules/openapi-generator/src/test/resources/3_0/oneof_discriminator_enum_shared_base.yaml b/modules/openapi-generator/src/test/resources/3_0/oneof_discriminator_enum_shared_base.yaml new file mode 100644 index 000000000000..0c9f5c9420e5 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/oneof_discriminator_enum_shared_base.yaml @@ -0,0 +1,79 @@ +openapi: 3.0.3 +info: + title: oneOf discriminator with shared base + description: > + A oneOf interface schema whose discriminator property is declared only on a + shared base schema that the concrete subtypes inherit via allOf. The oneOf + interface itself declares no properties, so the discriminator property type + must be resolved from the mapped child schemas (and through their allOf base) + rather than defaulting to string. + version: 1.0.0 +paths: + /pets: + post: + operationId: createPet + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PetRequest' + responses: + '201': + description: created +components: + schemas: + PetType: + type: string + enum: + - CAT + - DOG + description: Discriminator value identifying the type of pet + + # oneOf interface schema. It declares no properties of its own: the + # discriminator property lives on PetBase, which the children allOf-inherit. + PetRequest: + type: object + oneOf: + - $ref: '#/components/schemas/CatRequest' + - $ref: '#/components/schemas/DogRequest' + discriminator: + propertyName: petType + mapping: + CAT: '#/components/schemas/CatRequest' + DOG: '#/components/schemas/DogRequest' + + # Shared base WITHOUT a discriminator, so children flatten it via allOf + # instead of extending it (which would create a cyclical reference). + PetBase: + type: object + required: + - petType + - name + properties: + petType: + $ref: '#/components/schemas/PetType' + name: + type: string + + CatRequest: + type: object + allOf: + - $ref: '#/components/schemas/PetBase' + - type: object + required: + - indoor + properties: + indoor: + type: boolean + + DogRequest: + type: object + allOf: + - $ref: '#/components/schemas/PetBase' + - type: object + required: + - trained + properties: + trained: + type: boolean diff --git a/modules/openapi-generator/src/test/resources/3_0/oneof_discriminator_string_fallback.yaml b/modules/openapi-generator/src/test/resources/3_0/oneof_discriminator_string_fallback.yaml new file mode 100644 index 000000000000..cdc0ed8fd2f4 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/oneof_discriminator_string_fallback.yaml @@ -0,0 +1,60 @@ +openapi: 3.0.3 +info: + title: oneOf discriminator with no resolvable typed property + description: > + A oneOf interface whose discriminator property is a plain inline string on the + children (no $ref, no enum). There is no typed schema to resolve, so the + discriminator property type must fall back to "string". + version: 1.0.0 +paths: + /pets: + post: + operationId: createPet + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PetRequest' + responses: + '201': + description: created +components: + schemas: + PetRequest: + type: object + oneOf: + - $ref: '#/components/schemas/CatRequest' + - $ref: '#/components/schemas/DogRequest' + discriminator: + propertyName: petType + mapping: + CAT: '#/components/schemas/CatRequest' + DOG: '#/components/schemas/DogRequest' + + # Shared base whose discriminator property is a plain string (no $ref/enum). + PetBase: + type: object + required: + - petType + properties: + petType: + type: string + + CatRequest: + type: object + allOf: + - $ref: '#/components/schemas/PetBase' + - type: object + properties: + indoor: + type: boolean + + DogRequest: + type: object + allOf: + - $ref: '#/components/schemas/PetBase' + - type: object + properties: + trained: + type: boolean