Skip to content

Commit ac904ff

Browse files
[core] Add introspection support for discovering the discriminator type in a oneOf allOf polymorphic structure (#24158)
1 parent 0f13e90 commit ac904ff

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
/**
@@ -3349,7 +3354,7 @@ protected void setAddProps(Schema schema, IJsonSchemaValidationProperties proper
33493354
* @param sc The Schema that may contain the discriminator
33503355
* @param visitedSchemas An array list of visited schemas
33513356
*/
3352-
private Discriminator recursiveGetDiscriminator(Schema sc, ArrayList<Schema> visitedSchemas) {
3357+
private DiscriminatorData recursiveGetDiscriminator(Schema sc, ArrayList<Schema> visitedSchemas) {
33533358
return DiscriminatorUtils.recursiveGetDiscriminator(openAPI, this.getLegacyDiscriminatorBehavior(), sc, visitedSchemas);
33543359
}
33553360

@@ -3486,7 +3491,7 @@ protected List<MappedModel> getAllOfDescendants(String thisSchemaName) {
34863491
}
34873492

34883493
protected CodegenDiscriminator createDiscriminator(String schemaName, Schema schema) {
3489-
Discriminator sourceDiscriminator = recursiveGetDiscriminator(schema, new ArrayList<Schema>());
3494+
DiscriminatorData sourceDiscriminator = recursiveGetDiscriminator(schema, new ArrayList<Schema>());
34903495
if (sourceDiscriminator == null) {
34913496
return null;
34923497
}
@@ -3503,7 +3508,25 @@ protected CodegenDiscriminator createDiscriminator(String schemaName, Schema sch
35033508
// FIXME: there are other ways to define the type of the discriminator property (inline
35043509
// for example). Handling those scenarios is too complicated for me, I'm leaving it for
35053510
// the future..
3506-
discriminator.setPropertyType(getDiscriminatorPropertyType(schema, discriminatorPropertyName));
3511+
String discriminatorType;
3512+
Schema discSchema = sourceDiscriminator.getDiscriminatorSchema();
3513+
if (discSchema != null) {
3514+
if (ModelUtils.hasRef(discSchema)) {
3515+
discriminatorType = toModelName(ModelUtils.getSimpleRef(discSchema.get$ref()));
3516+
} else {
3517+
// Inline schema (inline enum, uri, ...): resolve via the same pipeline the
3518+
// concrete model classes use so the oneOf interface getter type matches.
3519+
// Keep String for typeless const to avoid resolving to Object.
3520+
if (ModelUtils.getType(discSchema) == null || discSchema.getEnum() == null) {
3521+
discriminatorType = getTypeDeclaration(discSchema);
3522+
} else {
3523+
discriminatorType = toEnumName(discriminatorPropertyName);
3524+
}
3525+
}
3526+
} else {
3527+
discriminatorType = getDiscriminatorPropertyType(schema, discriminatorPropertyName);
3528+
}
3529+
discriminator.setPropertyType(discriminatorType);
35073530

35083531
// check to see if the discriminator property is an enum string
35093532
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.jspecify.annotations.Nullable;
67
import org.openapitools.codegen.CodegenProperty;
78
import org.slf4j.Logger;
89
import org.slf4j.LoggerFactory;
@@ -17,6 +18,8 @@ public class DiscriminatorUtils {
1718

1819
private static final String CONFLICTING_DISCRIMINATOR_NAMES =
1920
"The alternative schemas have conflicting discriminator property names. The schemas must have the same property name, but found {}";
21+
private static final String CONFLICTING_DISCRIMINATOR_TYPES =
22+
"The alternative schemas have conflicting discriminator property types ({})";
2023
private static final String DEFINES_DISCRIMINATOR_BUT_REFERENCE_ALTERNATIVE_IS_MISSING =
2124
"'{}' defines discriminator '{}', but the referenced schema '{}' is missing {}";
2225
private static final String DEFINES_DISCRIMINATOR_BUT_ALTERNATIVE_HAS_OTHER_DEFINITION =
@@ -124,15 +127,19 @@ public static Optional<String> getDiscriminatorPropertyType(Schema schema, Strin
124127
* @param sc The Schema that may contain the discriminator
125128
* @param visitedSchemas an array list of visited schemas
126129
*/
127-
public static Discriminator recursiveGetDiscriminator(
130+
public static DiscriminatorData recursiveGetDiscriminator(
128131
OpenAPI openAPI,
129132
boolean legacyDiscriminatorBehavior,
130133
Schema sc,
131134
ArrayList<Schema> visitedSchemas) {
132135
Schema refSchema = ModelUtils.getReferencedSchema(openAPI, sc);
133-
Discriminator foundDisc = refSchema.getDiscriminator();
134-
if (foundDisc != null) {
135-
return foundDisc;
136+
DiscriminatorData foundDisc = new DiscriminatorData(refSchema.getDiscriminator(), null);
137+
if (foundDisc.getDiscriminator() != null) {
138+
String discriminatorPropertyName = foundDisc.getDiscriminator().getPropertyName();
139+
return new DiscriminatorData(
140+
foundDisc.getDiscriminator(),
141+
DiscriminatorUtils.getDiscriminatorSchema(refSchema, discriminatorPropertyName)
142+
);
136143
}
137144

138145
if (legacyDiscriminatorBehavior) {
@@ -156,7 +163,7 @@ public static Discriminator recursiveGetDiscriminator(
156163
if (foundDisc != null) {
157164
disc.setPropertyName(foundDisc.getPropertyName());
158165
disc.setMapping(foundDisc.getMapping());
159-
return disc;
166+
return new DiscriminatorData(disc, foundDisc.getDiscriminatorSchema());
160167
}
161168
}
162169
}
@@ -178,7 +185,7 @@ public static Discriminator recursiveGetDiscriminator(
178185
* An example of a sibling is an enum-ref that has its own description. This will lead to the enum being
179186
* referenced as an allOf that in turn has a ref, rather than a regular ref directly to the enum.
180187
*
181-
* @param schema The input OAS schema.
188+
* @param schema The input OAS schema that has the discriminator as a property.
182189
* @param discriminatorName The name of the discriminator property.
183190
*/
184191
private static Schema getDiscriminatorSchema(Schema schema, String discriminatorName) {
@@ -201,16 +208,17 @@ private static Schema getDiscriminatorSchema(Schema schema, String discriminator
201208
* @param visitedSchemas an array list of visited schemas
202209
* @return the discriminator if the alternatives correctly shares one, otherwise null
203210
*/
204-
private static Discriminator getDiscriminatorFromAlternatives(
211+
private static DiscriminatorData getDiscriminatorFromAlternatives(
205212
OpenAPI openAPI,
206213
boolean legacyDiscriminatorBehavior,
207214
List<Schema> alternativeSchemas,
208215
ArrayList<Schema> visitedSchemas) {
209216
Discriminator discriminator = new Discriminator();
210-
Discriminator foundDisc = null;
217+
DiscriminatorData foundDisc = null;
211218
Integer hasDiscriminatorCnt = 0;
212219
Integer hasNullTypeCnt = 0;
213220
Set<String> discriminatorsPropNames = new HashSet<>();
221+
Set<Schema> discriminatorTypes = new HashSet<>();
214222
for (Object alternative : alternativeSchemas) {
215223
if (ModelUtils.isNullType((Schema) alternative)) {
216224
// The null type does not have a discriminator. Skip.
@@ -221,16 +229,23 @@ private static Discriminator getDiscriminatorFromAlternatives(
221229
if (foundDisc != null) {
222230
discriminatorsPropNames.add(foundDisc.getPropertyName());
223231
hasDiscriminatorCnt++;
232+
if (foundDisc.getDiscriminatorSchema() != null) {
233+
discriminatorTypes.add(foundDisc.getDiscriminatorSchema());
234+
}
224235
}
225236
}
226237
if (discriminatorsPropNames.size() > 1) {
227238
once(LOGGER).warn(CONFLICTING_DISCRIMINATOR_NAMES, String.join(", ", discriminatorsPropNames));
228239
}
240+
if (discriminatorTypes.size() > 1) {
241+
once(LOGGER).warn(CONFLICTING_DISCRIMINATOR_TYPES, String.join(", ", discriminatorTypes.toString()));
242+
}
229243
boolean allAlternativesHaveADiscriminator = hasDiscriminatorCnt + hasNullTypeCnt == alternativeSchemas.size();
230244
if (foundDisc != null && allAlternativesHaveADiscriminator && discriminatorsPropNames.size() == 1) {
231245
discriminator.setPropertyName(foundDisc.getPropertyName());
232246
discriminator.setMapping(foundDisc.getMapping());
233-
return discriminator;
247+
Schema uniqueDiscriminatorType = discriminatorTypes.size() == 1 ? discriminatorTypes.iterator().next() : null;
248+
return new DiscriminatorData(discriminator, uniqueDiscriminatorType);
234249
}
235250
// If the scenario when composite schema has two children and one of them is the 'null' type,
236251
// there is no need for a discriminator.
@@ -271,4 +286,36 @@ private static CodegenProperty getDiscriminatorCodegenProperty(OpenAPI openAPI,
271286
return null;
272287
}
273288

289+
public static class DiscriminatorData {
290+
private final Discriminator discriminator;
291+
292+
@Nullable
293+
private final Schema discriminatorSchema;
294+
295+
public DiscriminatorData(Discriminator discriminator, @Nullable Schema discriminatorSchema) {
296+
this.discriminator = discriminator;
297+
this.discriminatorSchema = discriminatorSchema;
298+
}
299+
300+
public Discriminator getDiscriminator() {
301+
return discriminator;
302+
}
303+
304+
public @Nullable Schema getDiscriminatorSchema() {
305+
return discriminatorSchema;
306+
}
307+
308+
public String getPropertyName() {
309+
return discriminator.getPropertyName();
310+
}
311+
312+
public Map<String, String> getMapping() {
313+
return discriminator.getMapping();
314+
}
315+
316+
public Map<String, Object> getExtensions() {
317+
return discriminator.getExtensions();
318+
}
319+
}
320+
274321
}

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
@@ -2096,6 +2096,37 @@ public void testDiscriminatorWithMappingIssue14731() {
20962096
.content().doesNotContain("@JsonTypeName");
20972097
}
20982098

2099+
@Test
2100+
public void testOneOfInterfaceInheritedEnumDiscriminator() {
2101+
final Path output = newTempFolder();
2102+
final OpenAPI openAPI = new OpenAPIParser()
2103+
.readLocation("src/test/resources/3_0/oneOfDiscriminator.yaml", null, new ParseOptions())
2104+
.getOpenAPI();
2105+
2106+
// resttemplate (unlike the default okhttp-gson) renders oneOf as interfaces, so it uses the
2107+
// base Java/oneof_interface template that emits the discriminator getter type.
2108+
final JavaClientCodegen codegen = new JavaClientCodegen();
2109+
codegen.setLibrary("resttemplate");
2110+
codegen.setOutputDir(output.toString());
2111+
codegen.setUseOneOfInterfaces(true);
2112+
codegen.setLegacyDiscriminatorBehavior(false);
2113+
2114+
final ClientOptInput input = new ClientOptInput().openAPI(openAPI).config(codegen);
2115+
2116+
DefaultGenerator generator = new DefaultGenerator();
2117+
generator.setGeneratorPropertyDefault(CodegenConstants.MODELS, "true");
2118+
generator.setGeneratorPropertyDefault(CodegenConstants.APIS, "false");
2119+
generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_TESTS, "false");
2120+
generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_DOCS, "false");
2121+
generator.setGeneratorPropertyDefault(CodegenConstants.SUPPORTING_FILES, "false");
2122+
generator.opts(input).generate();
2123+
2124+
// Issue #22541: the inline-enum discriminator inherited from a base schema via allOf must
2125+
// resolve to the enum type in the oneOf interface getter, not String.
2126+
assertThat(output.resolve("src/main/java/org/openapitools/client/model/PetResponseEnumDisc.java"))
2127+
.content().contains("public PetTypeEnum getPetType();");
2128+
}
2129+
20992130
@Test
21002131
public void testDiscriminatorWithoutMappingIssue14731() {
21012132
final Path output = newTempFolder();

0 commit comments

Comments
 (0)