Skip to content

Commit a0934fe

Browse files
committed
[core] Resolve oneOf discriminator property type from child schemas
getDiscriminatorPropertyType resolved the discriminator property's type only from the oneOf/anyOf schema's own properties, defaulting to "string" when the property was not declared there. For a pure oneOf interface whose discriminator property lives on a shared base that the children inherit via allOf, this produced a String getter type that clashed with the enum the subtypes actually expose. Add the resolution to DiscriminatorUtils (alongside the existing getDiscriminatorPropertyType / getDiscriminatorSchema): getDiscriminatorPropertyType now falls back to the mapped child schemas, chasing the discriminator property through each child's allOf members (getDiscriminatorPropertyTypeFromChildren / getDiscriminatorSchemaDeep). The allOf descent tracks visited schemas to guard against a cyclic allOf composition recursing infinitely. The fallback only fires when the own-properties lookup is empty, so the type now resolves to the enum ref (e.g. PetType) when a child declares the discriminator property as a $ref, and otherwise still falls back to "string". DefaultCodegen.getDiscriminatorPropertyType becomes a thin binding that maps the resolved schema ref name to a generator-specific model name and applies the "string" default, as those depend on this codegen's instance state. The first resolution branch is unchanged, so existing specs are unaffected: regenerating the full sample suite produces no diffs.
1 parent 77c2e46 commit a0934fe

9 files changed

Lines changed: 569 additions & 4 deletions

File tree

modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3684,13 +3684,13 @@ protected Schema getDiscriminatorSchema(Schema schema, String discriminatorName)
36843684
}
36853685

36863686
/**
3687-
* Get the property type for the discriminator
3687+
* Get the property type for the discriminator from the schema, or its child schemas (from oneOf/anyOf refs) or returns String as a fallback
36883688
*
36893689
* @param schema The input OAS schema.
36903690
* @param discriminatorPropertyName The name of the discriminator property.
36913691
*/
36923692
protected String getDiscriminatorPropertyType(Schema schema, String discriminatorPropertyName) {
3693-
return DiscriminatorUtils.getDiscriminatorPropertyType(schema, discriminatorPropertyName)
3693+
return DiscriminatorUtils.getDiscriminatorPropertyType(openAPI, schema, discriminatorPropertyName)
36943694
.map(this::toModelName)
36953695
.orElseGet(() -> typeMapping.get("string"));
36963696
}

modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/DiscriminatorUtils.java

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,20 @@ public class DiscriminatorUtils {
1717
private static final String CONFLICTING_DISCRIMINATOR_NAMES =
1818
"The alternative schemas have conflicting discriminator property names. The schemas must have the same property name, but found {}";
1919

20+
private DiscriminatorUtils(){}
21+
2022
/**
2123
* Gets the simple ref name of the discriminator property type from the schema.
2224
*
2325
* @param schema The input OAS schema.
2426
* @param discriminatorPropertyName The name of the discriminator property.
2527
* @return referenced type name, or an empty optional if unavailable
2628
*/
27-
public static Optional<String> getDiscriminatorPropertyType(Schema schema, String discriminatorPropertyName) {
29+
public static Optional<String> getDiscriminatorPropertyType(OpenAPI openAPI, Schema schema, String discriminatorPropertyName) {
2830
return Optional.ofNullable(getDiscriminatorSchema(schema, discriminatorPropertyName))
2931
.map(Schema::get$ref)
30-
.map(ModelUtils::getSimpleRef);
32+
.map(ModelUtils::getSimpleRef)
33+
.or(()-> getDiscriminatorPropertyTypeFromChildren(openAPI, schema, discriminatorPropertyName));
3134
}
3235

3336
/**
@@ -49,6 +52,74 @@ public static Schema getDiscriminatorSchema(Schema schema, String discriminatorN
4952
return discSchema;
5053
}
5154

55+
/**
56+
* Resolve the discriminator property type by inspecting the oneOf/anyOf child schemas. Returns the simple
57+
* ref name of the first child that declares the discriminator property as a $ref (e.g. an enum), or an
58+
* empty optional if no child resolves to a typed property.
59+
*
60+
* @param openAPI the OpenAPI specification, used to resolve referenced child schemas.
61+
* @param schema The oneOf/anyOf interface schema.
62+
* @param discriminatorPropertyName The name of the discriminator property.
63+
*/
64+
static Optional<String> getDiscriminatorPropertyTypeFromChildren(OpenAPI openAPI, Schema schema, String discriminatorPropertyName) {
65+
List<Schema> children = new ArrayList<>();
66+
if (schema.getOneOf() != null) {
67+
children.addAll(schema.getOneOf());
68+
}
69+
if (schema.getAnyOf() != null) {
70+
children.addAll(schema.getAnyOf());
71+
}
72+
for (Schema child : children) {
73+
Schema resolved = ModelUtils.getReferencedSchema(openAPI, child);
74+
if (resolved == null) {
75+
continue;
76+
}
77+
Schema discSchema = getDiscriminatorSchemaDeep(openAPI, resolved, discriminatorPropertyName, new ArrayList<>());
78+
if (discSchema != null && discSchema.get$ref() != null) {
79+
return Optional.ofNullable(ModelUtils.getSimpleRef(discSchema.get$ref()));
80+
}
81+
}
82+
return Optional.empty();
83+
}
84+
85+
/**
86+
* Like {@link #getDiscriminatorSchema(Schema, String)}, but also chases the discriminator property through
87+
* a schema's allOf members. A oneOf child commonly carries the discriminator property indirectly, via an
88+
* allOf reference to a shared base schema rather than as a direct property.
89+
*
90+
* @param openAPI the OpenAPI specification, used to resolve referenced allOf members.
91+
* @param schema The schema to inspect.
92+
* @param discriminatorName The name of the discriminator property.
93+
* @param visited A list of schemas already visited in the recursion, to avoid infinite loops.
94+
* @return The discriminator property schema, or null if not found.
95+
*/
96+
static Schema getDiscriminatorSchemaDeep(OpenAPI openAPI, Schema schema, String discriminatorName, List<Schema> visited) {
97+
for (Schema s : visited) {
98+
if (s == schema) {
99+
return null;
100+
}
101+
}
102+
visited.add(schema);
103+
104+
Schema direct = getDiscriminatorSchema(schema, discriminatorName);
105+
if (direct != null) {
106+
return direct;
107+
}
108+
if (ModelUtils.isAllOf(schema)) {
109+
for (Object member : schema.getAllOf()) {
110+
Schema resolvedMember = ModelUtils.getReferencedSchema(openAPI, (Schema) member);
111+
if (resolvedMember == null) {
112+
continue;
113+
}
114+
Schema found = getDiscriminatorSchemaDeep(openAPI, resolvedMember, discriminatorName, visited);
115+
if (found != null) {
116+
return found;
117+
}
118+
}
119+
}
120+
return null;
121+
}
122+
52123
/**
53124
* Recursively look in Schema sc for the discriminator and return it
54125
*

modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1210,6 +1210,51 @@ public void testEnumDiscriminatorWithDescriptionOverridden3_1() {
12101210
assertTrue(fruitModel.getVars().get(0).isEnumRef);
12111211
}
12121212

1213+
@Test
1214+
public void testOneOfDiscriminatorTypeResolvedFromSharedBase() {
1215+
// The oneOf interface (PetRequest) declares no properties of its own; the discriminator
1216+
// property `petType` is declared only on the shared base (PetBase), which the children
1217+
// inherit via allOf. The discriminator property type must still resolve to the enum model
1218+
// (PetType) by inspecting the mapped child schemas, rather than falling back to "string".
1219+
final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/oneof_discriminator_enum_shared_base.yaml");
1220+
DefaultCodegen codegen = new DefaultCodegen();
1221+
codegen.setUseOneOfInterfaces(true);
1222+
codegen.setOpenAPI(openAPI);
1223+
1224+
Schema petRequest = openAPI.getComponents().getSchemas().get("PetRequest");
1225+
CodegenModel petRequestModel = codegen.fromModel("PetRequest", petRequest);
1226+
1227+
assertTrue(petRequestModel.getHasDiscriminatorWithNonEmptyMapping());
1228+
assertEquals("PetType", petRequestModel.discriminator.getPropertyType());
1229+
1230+
// The concrete subtypes resolve the same property type, so the generated getter signatures
1231+
// are consistent with the interface (no String-vs-enum return type clash).
1232+
Schema catRequest = openAPI.getComponents().getSchemas().get("CatRequest");
1233+
CodegenModel catRequestModel = codegen.fromModel("CatRequest", catRequest);
1234+
CodegenProperty petTypeVar = catRequestModel.getVars().stream()
1235+
.filter(v -> "petType".equals(v.baseName))
1236+
.findFirst()
1237+
.orElseThrow(() -> new AssertionError("petType property not found on CatRequest"));
1238+
assertEquals("PetType", petTypeVar.dataType);
1239+
}
1240+
1241+
@Test
1242+
public void testOneOfDiscriminatorTypeFallsBackToStringWhenNotTyped() {
1243+
// The discriminator property (petType) is declared only as a plain inline string on the children,
1244+
// with no $ref to a typed schema. There is nothing to resolve from the schema or its children, so
1245+
// the discriminator property type must fall back to "string".
1246+
final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/oneof_discriminator_string_fallback.yaml");
1247+
DefaultCodegen codegen = new DefaultCodegen();
1248+
codegen.setUseOneOfInterfaces(true);
1249+
codegen.setOpenAPI(openAPI);
1250+
1251+
Schema petRequest = openAPI.getComponents().getSchemas().get("PetRequest");
1252+
CodegenModel petRequestModel = codegen.fromModel("PetRequest", petRequest);
1253+
1254+
assertTrue(petRequestModel.getHasDiscriminatorWithNonEmptyMapping());
1255+
assertEquals("String", petRequestModel.discriminator.getPropertyType());
1256+
}
1257+
12131258
@Test
12141259
public void testParentName() {
12151260
final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/allOf.yaml");
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package org.openapitools.codegen.utils;
2+
3+
import io.swagger.v3.oas.models.OpenAPI;
4+
import io.swagger.v3.oas.models.media.Schema;
5+
import org.openapitools.codegen.TestUtils;
6+
import org.testng.annotations.Test;
7+
8+
import java.util.Optional;
9+
10+
import static org.testng.Assert.assertEquals;
11+
import static org.testng.Assert.assertFalse;
12+
13+
public class DiscriminatorUtilsTest {
14+
15+
/**
16+
* A pure oneOf interface schema declares no properties of its own, so resolving the discriminator
17+
* property type from the schema alone yields nothing. The type must instead be resolved from the
18+
* mapped child schemas, which inherit the discriminator property from a shared base via allOf.
19+
*/
20+
@Test
21+
public void resolvesDiscriminatorPropertyTypeFromChildrenWhenNotOnSchema() {
22+
final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/oneof_discriminator_enum_shared_base.yaml");
23+
Schema petRequest = openAPI.getComponents().getSchemas().get("PetRequest");
24+
25+
// The oneOf interface declares no discriminator property of its own, so the type is resolved from
26+
// the mapped children, finding the enum ref (PetType) declared on the shared base.
27+
Optional<String> resolved = DiscriminatorUtils.getDiscriminatorPropertyType(openAPI, petRequest, "petType");
28+
assertEquals(resolved.orElse(null), "PetType");
29+
}
30+
31+
/**
32+
* When the discriminator property is declared directly on the schema, the own-properties resolution is
33+
* used and the children are not consulted.
34+
*/
35+
@Test
36+
public void resolvesDiscriminatorPropertyTypeFromOwnPropertiesWhenPresent() {
37+
final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/oneof_discriminator_enum_shared_base.yaml");
38+
Schema petBase = openAPI.getComponents().getSchemas().get("PetBase");
39+
40+
Optional<String> resolved = DiscriminatorUtils.getDiscriminatorPropertyType(openAPI, petBase, "petType");
41+
assertEquals(resolved.orElse(null), "PetType");
42+
}
43+
44+
/**
45+
* The children fallback also resolves the discriminator property type when the interface uses anyOf
46+
* rather than oneOf.
47+
*/
48+
@Test
49+
public void resolvesDiscriminatorPropertyTypeFromAnyOfChildren() {
50+
final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/anyof_discriminator_enum_shared_base.yaml");
51+
Schema petRequest = openAPI.getComponents().getSchemas().get("PetRequest");
52+
53+
Optional<String> resolved = DiscriminatorUtils.getDiscriminatorPropertyType(openAPI, petRequest, "petType");
54+
assertEquals(resolved.orElse(null), "PetType");
55+
}
56+
57+
/**
58+
* The discriminator property is reached only by recursing through more than one level of allOf (the
59+
* children allOf an intermediate base that itself allOf the grandparent declaring the property).
60+
*/
61+
@Test
62+
public void resolvesDiscriminatorPropertyTypeThroughMultiLevelAllOf() {
63+
final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/oneof_discriminator_enum_nested_base.yaml");
64+
Schema petRequest = openAPI.getComponents().getSchemas().get("PetRequest");
65+
66+
Optional<String> resolved = DiscriminatorUtils.getDiscriminatorPropertyType(openAPI, petRequest, "petType");
67+
assertEquals(resolved.orElse(null), "PetType");
68+
}
69+
70+
/**
71+
* When neither the schema nor its children declare the discriminator property as a typed ($ref) schema
72+
* - the children carry it only as a plain inline string - there is nothing to resolve, so the lookup
73+
* returns empty (the caller then falls back to "string").
74+
*/
75+
@Test
76+
public void returnsEmptyWhenDiscriminatorPropertyHasNoTypedSchema() {
77+
final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/oneof_discriminator_string_fallback.yaml");
78+
Schema petRequest = openAPI.getComponents().getSchemas().get("PetRequest");
79+
80+
assertFalse(DiscriminatorUtils.getDiscriminatorPropertyType(openAPI, petRequest, "petType").isPresent());
81+
}
82+
83+
/**
84+
* A cyclic allOf composition (two bases that allOf each other) must not cause the allOf descent to
85+
* recurse infinitely. Resolution terminates; since no schema in the cycle declares the discriminator
86+
* property as a typed $ref, the lookup returns empty.
87+
*/
88+
@Test
89+
public void terminatesOnCyclicAllOfComposition() {
90+
final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/oneof_discriminator_cyclic_allof.yaml");
91+
Schema petRequest = openAPI.getComponents().getSchemas().get("PetRequest");
92+
93+
// Must return (not StackOverflowError) - the visited-set guard breaks the allOf cycle.
94+
assertFalse(DiscriminatorUtils.getDiscriminatorPropertyType(openAPI, petRequest, "petType").isPresent());
95+
}
96+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
openapi: 3.0.3
2+
info:
3+
title: anyOf discriminator with shared base
4+
description: >
5+
Same shape as the oneOf shared-base case, but the interface schema uses anyOf
6+
instead of oneOf. The discriminator property type must still be resolved from
7+
the mapped child schemas (through their allOf base), exercising the anyOf
8+
branch of the children fallback.
9+
version: 1.0.0
10+
paths:
11+
/pets:
12+
post:
13+
operationId: createPet
14+
requestBody:
15+
required: true
16+
content:
17+
application/json:
18+
schema:
19+
$ref: '#/components/schemas/PetRequest'
20+
responses:
21+
'201':
22+
description: created
23+
components:
24+
schemas:
25+
PetType:
26+
type: string
27+
enum:
28+
- CAT
29+
- DOG
30+
description: Discriminator value identifying the type of pet
31+
32+
PetRequest:
33+
type: object
34+
anyOf:
35+
- $ref: '#/components/schemas/CatRequest'
36+
- $ref: '#/components/schemas/DogRequest'
37+
discriminator:
38+
propertyName: petType
39+
mapping:
40+
CAT: '#/components/schemas/CatRequest'
41+
DOG: '#/components/schemas/DogRequest'
42+
43+
PetBase:
44+
type: object
45+
required:
46+
- petType
47+
- name
48+
properties:
49+
petType:
50+
$ref: '#/components/schemas/PetType'
51+
name:
52+
type: string
53+
54+
CatRequest:
55+
type: object
56+
allOf:
57+
- $ref: '#/components/schemas/PetBase'
58+
- type: object
59+
properties:
60+
indoor:
61+
type: boolean
62+
63+
DogRequest:
64+
type: object
65+
allOf:
66+
- $ref: '#/components/schemas/PetBase'
67+
- type: object
68+
properties:
69+
trained:
70+
type: boolean
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
openapi: 3.0.3
2+
info:
3+
title: oneOf discriminator with a cyclic allOf composition
4+
description: >
5+
The children allOf two bases (PetBaseA, PetBaseB) that allOf each other, forming
6+
a cycle. Resolving the discriminator property type must terminate instead of
7+
recursing infinitely; since no schema in the cycle declares the discriminator
8+
property as a typed $ref, the type falls back to "string".
9+
version: 1.0.0
10+
paths:
11+
/pets:
12+
post:
13+
operationId: createPet
14+
requestBody:
15+
required: true
16+
content:
17+
application/json:
18+
schema:
19+
$ref: '#/components/schemas/PetRequest'
20+
responses:
21+
'201':
22+
description: created
23+
components:
24+
schemas:
25+
PetRequest:
26+
type: object
27+
oneOf:
28+
- $ref: '#/components/schemas/CatRequest'
29+
- $ref: '#/components/schemas/DogRequest'
30+
discriminator:
31+
propertyName: petType
32+
mapping:
33+
CAT: '#/components/schemas/CatRequest'
34+
DOG: '#/components/schemas/DogRequest'
35+
36+
# Two bases that allOf each other -> cyclic allOf composition.
37+
PetBaseA:
38+
type: object
39+
allOf:
40+
- $ref: '#/components/schemas/PetBaseB'
41+
42+
PetBaseB:
43+
type: object
44+
allOf:
45+
- $ref: '#/components/schemas/PetBaseA'
46+
47+
CatRequest:
48+
type: object
49+
allOf:
50+
- $ref: '#/components/schemas/PetBaseA'
51+
- type: object
52+
properties:
53+
indoor:
54+
type: boolean
55+
56+
DogRequest:
57+
type: object
58+
allOf:
59+
- $ref: '#/components/schemas/PetBaseB'
60+
- type: object
61+
properties:
62+
trained:
63+
type: boolean

0 commit comments

Comments
 (0)