Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,20 @@ 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.
*
* @param schema The input OAS schema.
* @param discriminatorPropertyName The name of the discriminator property.
* @return referenced type name, or an empty optional if unavailable
*/
public static Optional<String> getDiscriminatorPropertyType(Schema schema, String discriminatorPropertyName) {
public static Optional<String> 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));
}

/**
Expand All @@ -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<String> getDiscriminatorPropertyTypeFromChildren(OpenAPI openAPI, Schema schema, String discriminatorPropertyName) {
List<Schema> 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<Schema> 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
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> 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<String> 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<String> 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<String> 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());
}
}
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading