Skip to content

Commit dd4ea86

Browse files
[core] Add introspection support for discovering the discriminator type in a oneOf allOf polymorphic structure
1 parent df45e93 commit dd4ea86

11 files changed

Lines changed: 470 additions & 27 deletions

File tree

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

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@
9393

9494
import static org.openapitools.codegen.CodegenConstants.*;
9595
import static org.openapitools.codegen.utils.CamelizeOption.LOWERCASE_FIRST_LETTER;
96+
import static org.openapitools.codegen.utils.DiscriminatorUtils.*;
9697
import static org.openapitools.codegen.utils.OnceLogger.once;
9798
import static org.openapitools.codegen.utils.StringUtils.*;
9899

@@ -1720,7 +1721,11 @@ public String toArrayModelParamName(String name) {
17201721
*/
17211722
@SuppressWarnings("static-method")
17221723
public String toEnumName(CodegenProperty property) {
1723-
return StringUtils.capitalize(property.name) + "Enum";
1724+
return toEnumName(property.name);
1725+
}
1726+
1727+
public String toEnumName(String propertyName) {
1728+
return StringUtils.capitalize(propertyName) + "Enum";
17241729
}
17251730

17261731
/**
@@ -3453,7 +3458,7 @@ private CodegenProperty discriminatorFound(String composedSchemaName, Schema sc,
34533458
* @param sc The Schema that may contain the discriminator
34543459
* @param visitedSchemas An array list of visited schemas
34553460
*/
3456-
private Discriminator recursiveGetDiscriminator(Schema sc, ArrayList<Schema> visitedSchemas) {
3461+
private DiscriminatorData recursiveGetDiscriminator(Schema sc, ArrayList<Schema> visitedSchemas) {
34573462
return DiscriminatorUtils.recursiveGetDiscriminator(openAPI, this.getLegacyDiscriminatorBehavior(), sc, visitedSchemas);
34583463
}
34593464

@@ -3590,7 +3595,7 @@ protected List<MappedModel> getAllOfDescendants(String thisSchemaName) {
35903595
}
35913596

35923597
protected CodegenDiscriminator createDiscriminator(String schemaName, Schema schema) {
3593-
Discriminator sourceDiscriminator = recursiveGetDiscriminator(schema, new ArrayList<Schema>());
3598+
DiscriminatorData sourceDiscriminator = recursiveGetDiscriminator(schema, new ArrayList<Schema>());
35943599
if (sourceDiscriminator == null) {
35953600
return null;
35963601
}
@@ -3607,7 +3612,25 @@ protected CodegenDiscriminator createDiscriminator(String schemaName, Schema sch
36073612
// FIXME: there are other ways to define the type of the discriminator property (inline
36083613
// for example). Handling those scenarios is too complicated for me, I'm leaving it for
36093614
// the future..
3610-
discriminator.setPropertyType(getDiscriminatorPropertyType(schema, discriminatorPropertyName));
3615+
String discriminatorType;
3616+
Schema discSchema = sourceDiscriminator.getDiscriminatorSchema();
3617+
if (discSchema != null) {
3618+
if (ModelUtils.hasRef(discSchema)) {
3619+
discriminatorType = toModelName(ModelUtils.getSimpleRef(discSchema.get$ref()));
3620+
} else {
3621+
// Inline schema (inline enum, uri, ...): resolve via the same pipeline the
3622+
// concrete model classes use so the oneOf interface getter type matches.
3623+
// Keep String for typeless const to avoid resolving to Object.
3624+
if (ModelUtils.getType(discSchema) == null || discSchema.getEnum() == null) {
3625+
discriminatorType = getTypeDeclaration(discSchema);
3626+
} else {
3627+
discriminatorType = toEnumName(discriminatorPropertyName);
3628+
}
3629+
}
3630+
} else {
3631+
discriminatorType = getDiscriminatorPropertyType(schema, discriminatorPropertyName);
3632+
}
3633+
discriminator.setPropertyType(discriminatorType);
36113634

36123635
// check to see if the discriminator property is an enum string
36133636
boolean isEnum = Optional

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

Lines changed: 56 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import io.swagger.v3.oas.models.OpenAPI;
44
import io.swagger.v3.oas.models.media.Discriminator;
55
import io.swagger.v3.oas.models.media.Schema;
6+
import org.checkerframework.checker.nullness.qual.Nullable;
67
import org.slf4j.Logger;
78
import org.slf4j.LoggerFactory;
89

@@ -16,6 +17,8 @@ public class DiscriminatorUtils {
1617

1718
private static final String CONFLICTING_DISCRIMINATOR_NAMES =
1819
"The alternative schemas have conflicting discriminator property names. The schemas must have the same property name, but found {}";
20+
private static final String CONFLICTING_DISCRIMINATOR_TYPES =
21+
"The alternative schemas have conflicting discriminator property types ({})";
1922

2023
/**
2124
* Gets the simple ref name of the discriminator property type from the schema.
@@ -35,7 +38,7 @@ public static Optional<String> getDiscriminatorPropertyType(Schema schema, Strin
3538
* An example of a sibling is an enum-ref that has its own description. This will lead to the enum being
3639
* referenced as an allOf that in turn has a ref, rather than a regular ref directly to the enum.
3740
*
38-
* @param schema The input OAS schema.
41+
* @param schema The input OAS schema that has the discriminator as a property.
3942
* @param discriminatorName The name of the discriminator property.
4043
*/
4144
public static Schema getDiscriminatorSchema(Schema schema, String discriminatorName) {
@@ -57,15 +60,19 @@ public static Schema getDiscriminatorSchema(Schema schema, String discriminatorN
5760
* @param sc The Schema that may contain the discriminator
5861
* @param visitedSchemas an array list of visited schemas
5962
*/
60-
public static Discriminator recursiveGetDiscriminator(
63+
public static DiscriminatorData recursiveGetDiscriminator(
6164
OpenAPI openAPI,
6265
boolean legacyDiscriminatorBehavior,
6366
Schema sc,
6467
ArrayList<Schema> visitedSchemas) {
6568
Schema refSchema = ModelUtils.getReferencedSchema(openAPI, sc);
66-
Discriminator foundDisc = refSchema.getDiscriminator();
67-
if (foundDisc != null) {
68-
return foundDisc;
69+
DiscriminatorData foundDisc = new DiscriminatorData(refSchema.getDiscriminator(), null);
70+
if (foundDisc.getDiscriminator() != null) {
71+
String discriminatorPropertyName = foundDisc.getDiscriminator().getPropertyName();
72+
return new DiscriminatorData(
73+
foundDisc.getDiscriminator(),
74+
DiscriminatorUtils.getDiscriminatorSchema(refSchema, discriminatorPropertyName)
75+
);
6976
}
7077

7178
if (legacyDiscriminatorBehavior) {
@@ -89,7 +96,7 @@ public static Discriminator recursiveGetDiscriminator(
8996
if (foundDisc != null) {
9097
disc.setPropertyName(foundDisc.getPropertyName());
9198
disc.setMapping(foundDisc.getMapping());
92-
return disc;
99+
return new DiscriminatorData(disc, foundDisc.getDiscriminatorSchema());
93100
}
94101
}
95102
}
@@ -115,16 +122,17 @@ public static Discriminator recursiveGetDiscriminator(
115122
* @param visitedSchemas an array list of visited schemas
116123
* @return the discriminator if the alternatives correctly shares one, otherwise null
117124
*/
118-
private static Discriminator getDiscriminatorFromAlternatives(
125+
private static DiscriminatorData getDiscriminatorFromAlternatives(
119126
OpenAPI openAPI,
120127
boolean legacyDiscriminatorBehavior,
121128
List<Schema> alternativeSchemas,
122129
ArrayList<Schema> visitedSchemas) {
123130
Discriminator discriminator = new Discriminator();
124-
Discriminator foundDisc = null;
131+
DiscriminatorData foundDisc = null;
125132
Integer hasDiscriminatorCnt = 0;
126133
Integer hasNullTypeCnt = 0;
127134
Set<String> discriminatorsPropNames = new HashSet<>();
135+
Set<Schema> discriminatorTypes = new HashSet<>();
128136
for (Object alternative : alternativeSchemas) {
129137
if (ModelUtils.isNullType((Schema) alternative)) {
130138
// The null type does not have a discriminator. Skip.
@@ -135,20 +143,59 @@ private static Discriminator getDiscriminatorFromAlternatives(
135143
if (foundDisc != null) {
136144
discriminatorsPropNames.add(foundDisc.getPropertyName());
137145
hasDiscriminatorCnt++;
146+
if (foundDisc.getDiscriminatorSchema() != null) {
147+
discriminatorTypes.add(foundDisc.getDiscriminatorSchema());
148+
}
138149
}
139150
}
140151
if (discriminatorsPropNames.size() > 1) {
141152
once(LOGGER).warn(CONFLICTING_DISCRIMINATOR_NAMES, String.join(", ", discriminatorsPropNames));
142153
}
154+
if (discriminatorTypes.size() > 1) {
155+
once(LOGGER).warn(CONFLICTING_DISCRIMINATOR_TYPES, String.join(", ", discriminatorTypes.toString()));
156+
}
143157
boolean allAlternativesHaveADiscriminator = hasDiscriminatorCnt + hasNullTypeCnt == alternativeSchemas.size();
144158
if (foundDisc != null && allAlternativesHaveADiscriminator && discriminatorsPropNames.size() == 1) {
145159
discriminator.setPropertyName(foundDisc.getPropertyName());
146160
discriminator.setMapping(foundDisc.getMapping());
147-
return discriminator;
161+
Schema uniqueDiscriminatorType = discriminatorTypes.size() == 1 ? discriminatorTypes.iterator().next() : null;
162+
return new DiscriminatorData(discriminator, uniqueDiscriminatorType);
148163
}
149164
// If the scenario when composite schema has two children and one of them is the 'null' type,
150165
// there is no need for a discriminator.
151166
return null;
152167
}
153168

169+
public static class DiscriminatorData {
170+
private final Discriminator discriminator;
171+
172+
@Nullable
173+
private final Schema discriminatorSchema;
174+
175+
public DiscriminatorData(Discriminator discriminator, @Nullable Schema discriminatorSchema) {
176+
this.discriminator = discriminator;
177+
this.discriminatorSchema = discriminatorSchema;
178+
}
179+
180+
public Discriminator getDiscriminator() {
181+
return discriminator;
182+
}
183+
184+
public @Nullable Schema getDiscriminatorSchema() {
185+
return discriminatorSchema;
186+
}
187+
188+
public String getPropertyName() {
189+
return discriminator.getPropertyName();
190+
}
191+
192+
public Map<String, String> getMapping() {
193+
return discriminator.getMapping();
194+
}
195+
196+
public Map<String, Object> getExtensions() {
197+
return discriminator.getExtensions();
198+
}
199+
}
200+
154201
}

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2392,6 +2392,16 @@ public static boolean hasAnyOf(Schema schema) {
23922392
return false;
23932393
}
23942394

2395+
/**
2396+
* Returns true if the schema contains a $ref
2397+
*
2398+
* @param schema the schema
2399+
* @return true if $ref is set
2400+
*/
2401+
public static boolean hasRef(Schema schema) {
2402+
return schema != null && schema.get$ref() != null;
2403+
}
2404+
23952405
/**
23962406
* Returns schema type.
23972407
* For 3.1 spec, return the first one.

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

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,21 +15,21 @@
1515
* This class holds data to add to `oneOf` members. Let's consider this example:
1616
* <p>
1717
* Foo:
18-
* properties:
19-
* x:
20-
* oneOf:
21-
* - $ref: "#/components/schemas/One
22-
* - $ref: "#/components/schemas/Two
23-
* y:
24-
* type: string
18+
* properties:
19+
* x:
20+
* oneOf:
21+
* - $ref: "#/components/schemas/One"
22+
* - $ref: "#/components/schemas/Two"
23+
* y:
24+
* type: string
2525
* One:
26-
* properties:
27-
* z:
28-
* type: string
26+
* properties:
27+
* z:
28+
* type: string
2929
* Two:
30-
* properties:
31-
* a:
32-
* type: string
30+
* properties:
31+
* a:
32+
* type: string
3333
* <p>
3434
* In codegens that use this mechanism, `Foo` will become an interface and `One` will
3535
* become its implementing class. This class carries all data necessary to properly modify
@@ -99,7 +99,7 @@ public void addFromInterfaceModel(CodegenModel cm, List<Map<String, String>> mod
9999
* @param cc CodegenConfig running this operation
100100
* @param implcm the implementing model
101101
* @param implImports imports of the implementing model
102-
* @param addInterfaceImports whether or not to add the interface model as import (will vary by language)
102+
* @param addInterfaceImports whether to add the interface model as import (will vary by language)
103103
*/
104104
@SuppressWarnings("unchecked")
105105
public void addToImplementor(CodegenConfig cc, CodegenModel implcm, List<Map<String, String>> implImports, boolean addInterfaceImports) {

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

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

1213+
@Test
1214+
public void testOneOfAllOfEnumRefDiscriminatorInheritance() {
1215+
final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/oneOfDiscriminator.yaml");
1216+
new OpenAPINormalizer(openAPI, Map.of()).normalize();
1217+
DefaultCodegen codegen = new DefaultCodegen();
1218+
codegen.setUseOneOfInterfaces(true);
1219+
codegen.setLegacyDiscriminatorBehavior(false);
1220+
1221+
Schema inner = openAPI.getComponents().getSchemas().get("VehicleResponse");
1222+
codegen.setOpenAPI(openAPI);
1223+
CodegenModel innerModel = codegen.fromModel("VehicleResponse", inner);
1224+
assertTrue(innerModel.getHasDiscriminatorWithNonEmptyMapping());
1225+
assertTrue(innerModel.discriminator.getIsEnum());
1226+
assertEquals("VehicleType", innerModel.discriminator.getPropertyType());
1227+
assertTrue(innerModel.getVars().get(0).isEnumRef);
1228+
1229+
Schema car = openAPI.getComponents().getSchemas().get("Car");
1230+
CodegenModel carModel = codegen.fromModel("Car", car);
1231+
assertTrue(carModel.discriminator.getIsEnum());
1232+
assertEquals("VehicleType", carModel.discriminator.getPropertyType());
1233+
assertTrue(carModel.getVars().get(0).isEnumRef);
1234+
1235+
Schema bike = openAPI.getComponents().getSchemas().get("Bike");
1236+
CodegenModel bikeModel = codegen.fromModel("Bike", bike);
1237+
assertTrue(bikeModel.discriminator.getIsEnum());
1238+
assertEquals("VehicleType", bikeModel.discriminator.getPropertyType());
1239+
assertTrue(bikeModel.getVars().get(0).isEnumRef);
1240+
}
1241+
1242+
@Test
1243+
public void testOneOfAllOfInlineEnumDiscriminatorInheritance() {
1244+
final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/oneOfDiscriminator.yaml");
1245+
new OpenAPINormalizer(openAPI, Map.of()).normalize();
1246+
DefaultCodegen codegen = new DefaultCodegen();
1247+
codegen.setUseOneOfInterfaces(true);
1248+
codegen.setLegacyDiscriminatorBehavior(false);
1249+
1250+
Schema inner = openAPI.getComponents().getSchemas().get("PetResponseEnumDisc");
1251+
codegen.setOpenAPI(openAPI);
1252+
CodegenModel innerModel = codegen.fromModel("PetResponseEnumDisc", inner);
1253+
assertTrue(innerModel.getHasDiscriminatorWithNonEmptyMapping());
1254+
assertTrue(innerModel.discriminator.getIsEnum());
1255+
assertEquals("PetTypeEnum", innerModel.discriminator.getPropertyType());
1256+
assertFalse(innerModel.getVars().get(0).isEnumRef);
1257+
1258+
Schema dog = openAPI.getComponents().getSchemas().get("DogEnumDisc");
1259+
CodegenModel dogModel = codegen.fromModel("DogEnumDisc", dog);
1260+
assertTrue(dogModel.discriminator.getIsEnum());
1261+
assertEquals("PetTypeEnum", dogModel.discriminator.getPropertyType());
1262+
assertFalse(dogModel.getVars().get(0).isEnumRef);
1263+
1264+
Schema cat = openAPI.getComponents().getSchemas().get("CatEnumDisc");
1265+
CodegenModel catModel = codegen.fromModel("CatEnumDisc", cat);
1266+
assertTrue(catModel.discriminator.getIsEnum());
1267+
assertEquals("PetTypeEnum", catModel.discriminator.getPropertyType());
1268+
assertFalse(catModel.getVars().get(0).isEnumRef);
1269+
}
1270+
1271+
@Test
1272+
public void testOneOfAllOfInlineStringWithFormatDiscriminatorInheritance() {
1273+
final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/oneOfDiscriminator.yaml");
1274+
new OpenAPINormalizer(openAPI, Map.of()).normalize();
1275+
DefaultCodegen codegen = new DefaultCodegen();
1276+
codegen.setUseOneOfInterfaces(true);
1277+
codegen.setLegacyDiscriminatorBehavior(false);
1278+
1279+
Schema inner = openAPI.getComponents().getSchemas().get("PetResponseUriDisc");
1280+
codegen.setOpenAPI(openAPI);
1281+
CodegenModel innerModel = codegen.fromModel("PetResponseUriDisc", inner);
1282+
assertTrue(innerModel.getHasDiscriminatorWithNonEmptyMapping());
1283+
assertEquals("URI", innerModel.discriminator.getPropertyType());
1284+
1285+
Schema dog = openAPI.getComponents().getSchemas().get("DogUriDisc");
1286+
CodegenModel dogModel = codegen.fromModel("DogUriDisc", dog);
1287+
assertEquals("URI", dogModel.discriminator.getPropertyType());
1288+
1289+
Schema cat = openAPI.getComponents().getSchemas().get("CatUriDisc");
1290+
CodegenModel catModel = codegen.fromModel("CatUriDisc", cat);
1291+
assertEquals("URI", catModel.discriminator.getPropertyType());
1292+
}
1293+
12131294
@Test
12141295
public void testParentName() {
12151296
final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/allOf.yaml");

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

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2038,6 +2038,37 @@ public void testDiscriminatorWithMappingIssue14731() {
20382038
.content().doesNotContain("@JsonTypeName");
20392039
}
20402040

2041+
@Test
2042+
public void testOneOfInterfaceInheritedEnumDiscriminator() {
2043+
final Path output = newTempFolder();
2044+
final OpenAPI openAPI = new OpenAPIParser()
2045+
.readLocation("src/test/resources/3_0/oneOfDiscriminator.yaml", null, new ParseOptions())
2046+
.getOpenAPI();
2047+
2048+
// resttemplate (unlike the default okhttp-gson) renders oneOf as interfaces, so it uses the
2049+
// base Java/oneof_interface template that emits the discriminator getter type.
2050+
final JavaClientCodegen codegen = new JavaClientCodegen();
2051+
codegen.setLibrary("resttemplate");
2052+
codegen.setOutputDir(output.toString());
2053+
codegen.setUseOneOfInterfaces(true);
2054+
codegen.setLegacyDiscriminatorBehavior(false);
2055+
2056+
final ClientOptInput input = new ClientOptInput().openAPI(openAPI).config(codegen);
2057+
2058+
DefaultGenerator generator = new DefaultGenerator();
2059+
generator.setGeneratorPropertyDefault(CodegenConstants.MODELS, "true");
2060+
generator.setGeneratorPropertyDefault(CodegenConstants.APIS, "false");
2061+
generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_TESTS, "false");
2062+
generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_DOCS, "false");
2063+
generator.setGeneratorPropertyDefault(CodegenConstants.SUPPORTING_FILES, "false");
2064+
generator.opts(input).generate();
2065+
2066+
// Issue #22541: the inline-enum discriminator inherited from a base schema via allOf must
2067+
// resolve to the enum type in the oneOf interface getter, not String.
2068+
assertThat(output.resolve("src/main/java/org/openapitools/client/model/PetResponseEnumDisc.java"))
2069+
.content().contains("public PetTypeEnum getPetType();");
2070+
}
2071+
20412072
@Test
20422073
public void testDiscriminatorWithoutMappingIssue14731() {
20432074
final Path output = newTempFolder();

0 commit comments

Comments
 (0)