Skip to content

Commit 1c1a852

Browse files
Add introspection support for discovering an enum discriminator in a oneOf allOf polymorphic structure
1 parent d6095a0 commit 1c1a852

7 files changed

Lines changed: 176 additions & 27 deletions

File tree

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

Lines changed: 10 additions & 3 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

@@ -3453,7 +3454,7 @@ private CodegenProperty discriminatorFound(String composedSchemaName, Schema sc,
34533454
* @param sc The Schema that may contain the discriminator
34543455
* @param visitedSchemas An array list of visited schemas
34553456
*/
3456-
private Discriminator recursiveGetDiscriminator(Schema sc, ArrayList<Schema> visitedSchemas) {
3457+
private DiscriminatorData recursiveGetDiscriminator(Schema sc, ArrayList<Schema> visitedSchemas) {
34573458
return DiscriminatorUtils.recursiveGetDiscriminator(openAPI, this.getLegacyDiscriminatorBehavior(), sc, visitedSchemas);
34583459
}
34593460

@@ -3590,7 +3591,7 @@ protected List<MappedModel> getAllOfDescendants(String thisSchemaName) {
35903591
}
35913592

35923593
protected CodegenDiscriminator createDiscriminator(String schemaName, Schema schema) {
3593-
Discriminator sourceDiscriminator = recursiveGetDiscriminator(schema, new ArrayList<Schema>());
3594+
DiscriminatorData sourceDiscriminator = recursiveGetDiscriminator(schema, new ArrayList<Schema>());
35943595
if (sourceDiscriminator == null) {
35953596
return null;
35963597
}
@@ -3607,7 +3608,13 @@ protected CodegenDiscriminator createDiscriminator(String schemaName, Schema sch
36073608
// FIXME: there are other ways to define the type of the discriminator property (inline
36083609
// for example). Handling those scenarios is too complicated for me, I'm leaving it for
36093610
// the future..
3610-
discriminator.setPropertyType(getDiscriminatorPropertyType(schema, discriminatorPropertyName));
3611+
String discriminatorType;
3612+
if (sourceDiscriminator.getPropertyType() != null) {
3613+
discriminatorType = toModelName(sourceDiscriminator.getPropertyType());
3614+
} else {
3615+
discriminatorType = getDiscriminatorPropertyType(schema, discriminatorPropertyName);
3616+
}
3617+
discriminator.setPropertyType(discriminatorType);
36113618

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

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

Lines changed: 56 additions & 8 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.
@@ -57,15 +60,20 @@ 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.getDiscriminatorPropertyType(refSchema, discriminatorPropertyName)
75+
.orElse(null)
76+
);
6977
}
7078

7179
if (legacyDiscriminatorBehavior) {
@@ -89,7 +97,7 @@ public static Discriminator recursiveGetDiscriminator(
8997
if (foundDisc != null) {
9098
disc.setPropertyName(foundDisc.getPropertyName());
9199
disc.setMapping(foundDisc.getMapping());
92-
return disc;
100+
return new DiscriminatorData(disc, foundDisc.getPropertyType());
93101
}
94102
}
95103
}
@@ -115,16 +123,17 @@ public static Discriminator recursiveGetDiscriminator(
115123
* @param visitedSchemas an array list of visited schemas
116124
* @return the discriminator if the alternatives correctly shares one, otherwise null
117125
*/
118-
private static Discriminator getDiscriminatorFromAlternatives(
126+
private static DiscriminatorData getDiscriminatorFromAlternatives(
119127
OpenAPI openAPI,
120128
boolean legacyDiscriminatorBehavior,
121129
List<Schema> alternativeSchemas,
122130
ArrayList<Schema> visitedSchemas) {
123131
Discriminator discriminator = new Discriminator();
124-
Discriminator foundDisc = null;
132+
DiscriminatorData foundDisc = null;
125133
Integer hasDiscriminatorCnt = 0;
126134
Integer hasNullTypeCnt = 0;
127135
Set<String> discriminatorsPropNames = new HashSet<>();
136+
Set<String> discriminatorTypes = new HashSet<>();
128137
for (Object alternative : alternativeSchemas) {
129138
if (ModelUtils.isNullType((Schema) alternative)) {
130139
// The null type does not have a discriminator. Skip.
@@ -135,20 +144,59 @@ private static Discriminator getDiscriminatorFromAlternatives(
135144
if (foundDisc != null) {
136145
discriminatorsPropNames.add(foundDisc.getPropertyName());
137146
hasDiscriminatorCnt++;
147+
if (foundDisc.getPropertyType() != null) {
148+
discriminatorTypes.add(foundDisc.getPropertyType());
149+
}
138150
}
139151
}
140152
if (discriminatorsPropNames.size() > 1) {
141153
once(LOGGER).warn(CONFLICTING_DISCRIMINATOR_NAMES, String.join(", ", discriminatorsPropNames));
142154
}
155+
if (discriminatorTypes.size() > 1) {
156+
once(LOGGER).warn(CONFLICTING_DISCRIMINATOR_TYPES, String.join(", ", discriminatorTypes));
157+
}
143158
boolean allAlternativesHaveADiscriminator = hasDiscriminatorCnt + hasNullTypeCnt == alternativeSchemas.size();
144159
if (foundDisc != null && allAlternativesHaveADiscriminator && discriminatorsPropNames.size() == 1) {
145160
discriminator.setPropertyName(foundDisc.getPropertyName());
146161
discriminator.setMapping(foundDisc.getMapping());
147-
return discriminator;
162+
String uniqueDiscriminatorType = discriminatorTypes.size() == 1 ? discriminatorTypes.iterator().next() : null;
163+
return new DiscriminatorData(discriminator, uniqueDiscriminatorType);
148164
}
149165
// If the scenario when composite schema has two children and one of them is the 'null' type,
150166
// there is no need for a discriminator.
151167
return null;
152168
}
153169

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

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: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1210,6 +1210,35 @@ public void testEnumDiscriminatorWithDescriptionOverridden3_1() {
12101210
assertTrue(fruitModel.getVars().get(0).isEnumRef);
12111211
}
12121212

1213+
@Test
1214+
public void testOneOfAllOfEnumDiscriminatorInheritance() {
1215+
final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_1/polymorphism-oneof-allof-and-enum-discriminator.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("getAllPets_200_response_inner");
1222+
codegen.setOpenAPI(openAPI);
1223+
CodegenModel innerModel = codegen.fromModel("getAllPets_200_response_inner", inner);
1224+
assertTrue(innerModel.getHasDiscriminatorWithNonEmptyMapping());
1225+
assertTrue(innerModel.discriminator.getIsEnum());
1226+
assertEquals("PetType", innerModel.discriminator.getPropertyType());
1227+
assertTrue(innerModel.getVars().get(0).isEnumRef);
1228+
1229+
Schema cat = openAPI.getComponents().getSchemas().get("Cat");
1230+
CodegenModel catModel = codegen.fromModel("Cat", cat);
1231+
assertTrue(catModel.discriminator.getIsEnum());
1232+
assertEquals("PetType", catModel.discriminator.getPropertyType());
1233+
assertTrue(catModel.getVars().get(0).isEnumRef);
1234+
1235+
Schema dog = openAPI.getComponents().getSchemas().get("Dog");
1236+
CodegenModel dogModel = codegen.fromModel("Dog", dog);
1237+
assertTrue(dogModel.discriminator.getIsEnum());
1238+
assertEquals("PetType", dogModel.discriminator.getPropertyType());
1239+
assertTrue(dogModel.getVars().get(0).isEnumRef);
1240+
}
1241+
12131242
@Test
12141243
public void testParentName() {
12151244
final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/allOf.yaml");
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
openapi: 3.1.0
2+
info:
3+
title: Polymorphism example with oneOf, allOf and enum-discriminator
4+
version: "1.0"
5+
paths:
6+
/pets:
7+
get:
8+
operationId: getAllPets
9+
responses:
10+
"200":
11+
content:
12+
application/json:
13+
schema:
14+
type: array
15+
items:
16+
oneOf:
17+
- $ref: "#/components/schemas/Cat"
18+
- $ref: "#/components/schemas/Dog"
19+
description: Success
20+
components:
21+
schemas:
22+
PetType:
23+
type: string
24+
enum:
25+
- Cat
26+
- Dog
27+
Pet:
28+
type: object
29+
discriminator:
30+
propertyName: petType
31+
properties:
32+
petType:
33+
$ref: '#/components/schemas/PetType'
34+
required:
35+
- petType
36+
Cat:
37+
description: A representation of a cat
38+
allOf:
39+
- $ref: '#/components/schemas/Pet'
40+
- type: object
41+
properties:
42+
huntingSkill:
43+
type: string
44+
description: The measured skill for hunting
45+
enum:
46+
- clueless
47+
- lazy
48+
- adventurous
49+
- aggressive
50+
required:
51+
- huntingSkill
52+
Dog:
53+
description: A representation of a dog
54+
allOf:
55+
- $ref: '#/components/schemas/Pet'
56+
- type: object
57+
properties:
58+
packSize:
59+
type: integer
60+
format: int32
61+
description: the size of the pack the dog is from
62+
default: 0
63+
minimum: 0
64+
required:
65+
- packSize

samples/client/petstore/typescript-fetch/builds/kebab-case/models/enum-test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ export type EnumTestEnumNumberEnum = typeof EnumTestEnumNumberEnum[keyof typeof
142142
* Check if a given object implements the EnumTest interface.
143143
*/
144144
export function instanceOfEnumTest(value: object): value is EnumTest {
145-
if ((!('enumStringRequired' in value) && !('enum_string_required' in value)) || (value['enumStringRequired'] === undefined && value['enum_string_required'] === undefined)) return false;
145+
if ((!('enumStringRequired' in (value as Record<string, any>)) && !('enum_string_required' in (value as Record<string, any>))) || ((value as Record<string, any>)['enumStringRequired'] === undefined && (value as Record<string, any>)['enum_string_required'] === undefined)) return false;
146146
return true;
147147
}
148148

samples/client/petstore/typescript-fetch/builds/kebab-case/models/format-test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ export interface FormatTest {
122122
*/
123123
export function instanceOfFormatTest(value: object): value is FormatTest {
124124
if (!('number' in value) || value['number'] === undefined) return false;
125-
if ((!('_byte' in value) && !('byte' in value)) || (value['_byte'] === undefined && value['byte'] === undefined)) return false;
125+
if ((!('_byte' in (value as Record<string, any>)) && !('byte' in (value as Record<string, any>))) || ((value as Record<string, any>)['_byte'] === undefined && (value as Record<string, any>)['byte'] === undefined)) return false;
126126
if (!('date' in value) || value['date'] === undefined) return false;
127127
if (!('password' in value) || value['password'] === undefined) return false;
128128
return true;

0 commit comments

Comments
 (0)