Skip to content

Commit 072bba1

Browse files
committed
Add SIMPLIFY_ONEOF_ENUM normalization rule to merge oneOf enum schemas
1 parent afc27ef commit 072bba1

File tree

2 files changed

+190
-0
lines changed

2 files changed

+190
-0
lines changed

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

+116
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ public class OpenAPINormalizer {
8181

8282
// when set to true, boolean enum will be converted to just boolean
8383
final String SIMPLIFY_BOOLEAN_ENUM = "SIMPLIFY_BOOLEAN_ENUM";
84+
85+
// when set to true, oneOf with multiple enum schemas will be merged into a single enum schema
86+
// even if one of them is an object
87+
final String SIMPLIFY_ONEOF_ENUM = "SIMPLIFY_ONEOF_ENUM";
8488

8589
// when set to a string value, tags in all operations will be reset to the string value provided
8690
final String SET_TAGS_FOR_ALL_OPERATIONS = "SET_TAGS_FOR_ALL_OPERATIONS";
@@ -159,6 +163,7 @@ public OpenAPINormalizer(OpenAPI openAPI, Map<String, String> inputRules) {
159163
ruleNames.add(SIMPLIFY_ANYOF_STRING_AND_ENUM_STRING);
160164
ruleNames.add(SIMPLIFY_ONEOF_ANYOF);
161165
ruleNames.add(SIMPLIFY_BOOLEAN_ENUM);
166+
ruleNames.add(SIMPLIFY_ONEOF_ENUM);
162167
ruleNames.add(KEEP_ONLY_FIRST_TAG_IN_OPERATION);
163168
ruleNames.add(SET_TAGS_FOR_ALL_OPERATIONS);
164169
ruleNames.add(SET_TAGS_TO_OPERATIONID);
@@ -176,6 +181,7 @@ public OpenAPINormalizer(OpenAPI openAPI, Map<String, String> inputRules) {
176181
// rules that are default to true
177182
rules.put(SIMPLIFY_ONEOF_ANYOF, true);
178183
rules.put(SIMPLIFY_BOOLEAN_ENUM, true);
184+
rules.put(SIMPLIFY_ONEOF_ENUM, true);
179185

180186
processRules(inputRules);
181187

@@ -787,6 +793,9 @@ private Schema normalizeAllOfWithProperties(Schema schema, Set<Schema> visitedSc
787793
private Schema normalizeOneOf(Schema schema, Set<Schema> visitedSchemas) {
788794
// simplify first as the schema may no longer be a oneOf after processing the rule below
789795
schema = processSimplifyOneOf(schema);
796+
797+
// try to merge enum schemas
798+
schema = processSimplifyOneOfEnum(schema, visitedSchemas);
790799

791800
// if it's still a oneOf, loop through the sub-schemas
792801
if (schema.getOneOf() != null) {
@@ -1304,6 +1313,113 @@ private void processSimplifyBooleanEnum(Schema schema) {
13041313
}
13051314
}
13061315
}
1316+
1317+
/**
1318+
* If the schema is oneOf with multiple enum schemas, merge them into a single enum schema
1319+
* even if one of them is an object.
1320+
*
1321+
* @param schema Schema
1322+
* @param visitedSchemas a set of visited schemas
1323+
* @return Schema
1324+
*/
1325+
private Schema processSimplifyOneOfEnum(Schema schema, Set<Schema> visitedSchemas) {
1326+
if (!getRule(SIMPLIFY_ONEOF_ENUM)) {
1327+
return schema;
1328+
}
1329+
1330+
List<Schema> oneOfSchemas = schema.getOneOf();
1331+
if (oneOfSchemas == null || oneOfSchemas.size() <= 1) {
1332+
return schema;
1333+
}
1334+
1335+
// Check if all schemas are either objects or have enums
1336+
boolean allEnumSchemas = true;
1337+
List<Object> allEnumValues = new ArrayList<>();
1338+
StringSchema mergedSchema = null;
1339+
1340+
for (Schema subSchema : oneOfSchemas) {
1341+
subSchema = ModelUtils.getReferencedSchema(openAPI, subSchema);
1342+
1343+
if (subSchema instanceof StringSchema && ((StringSchema)subSchema).getEnum() != null) {
1344+
if (mergedSchema == null) {
1345+
// Use the first StringSchema as our template
1346+
mergedSchema = new StringSchema();
1347+
mergedSchema.setDescription(schema.getDescription());
1348+
mergedSchema.setExample(schema.getExample());
1349+
mergedSchema.setExamples(schema.getExamples());
1350+
mergedSchema.setNullable(schema.getNullable());
1351+
mergedSchema.setDefault(schema.getDefault());
1352+
mergedSchema.setDeprecated(schema.getDeprecated());
1353+
}
1354+
// Add all enum values from this schema
1355+
allEnumValues.addAll(((StringSchema)subSchema).getEnum());
1356+
} else if (ModelUtils.isObjectSchema(subSchema)) {
1357+
// If it's an object, we'll consider it valid for merging
1358+
// but we need to extract its type name as an enum value
1359+
1360+
// Get schema name or create a placeholder
1361+
String objectEnumValue = determineObjectEnumName(subSchema);
1362+
if (objectEnumValue != null) {
1363+
if (mergedSchema == null) {
1364+
mergedSchema = new StringSchema();
1365+
mergedSchema.setDescription(schema.getDescription());
1366+
mergedSchema.setExample(schema.getExample());
1367+
mergedSchema.setExamples(schema.getExamples());
1368+
mergedSchema.setNullable(schema.getNullable());
1369+
mergedSchema.setDefault(schema.getDefault());
1370+
mergedSchema.setDeprecated(schema.getDeprecated());
1371+
}
1372+
allEnumValues.add(objectEnumValue);
1373+
} else {
1374+
// If we can't determine a name, we can't merge
1375+
allEnumSchemas = false;
1376+
break;
1377+
}
1378+
} else {
1379+
// This schema is not an enum or object, can't merge
1380+
allEnumSchemas = false;
1381+
break;
1382+
}
1383+
}
1384+
1385+
if (allEnumSchemas && mergedSchema != null && !allEnumValues.isEmpty()) {
1386+
// Remove duplicates and convert to strings
1387+
Set<String> uniqueEnumValues = new LinkedHashSet<>();
1388+
for (Object value : allEnumValues) {
1389+
uniqueEnumValues.add(value.toString());
1390+
}
1391+
mergedSchema.setEnum(new ArrayList<>(uniqueEnumValues));
1392+
1393+
LOGGER.debug("Merged {} oneOf enum schemas into a single enum schema with values: {}",
1394+
oneOfSchemas.size(), uniqueEnumValues);
1395+
1396+
return mergedSchema;
1397+
}
1398+
1399+
return schema;
1400+
}
1401+
1402+
/**
1403+
* Determines a meaningful enum value name for an object schema
1404+
*
1405+
* @param schema The object schema to determine a name for
1406+
* @return A string representing the object name, or null if can't be determined
1407+
*/
1408+
private String determineObjectEnumName(Schema schema) {
1409+
// Try to use title first
1410+
if (schema.getTitle() != null) {
1411+
return schema.getTitle();
1412+
}
1413+
1414+
// Try to use type or $ref name
1415+
if (schema.get$ref() != null) {
1416+
String ref = ModelUtils.getSimpleRef(schema.get$ref());
1417+
return ref;
1418+
}
1419+
1420+
// If no clear name, use a generic placeholder for an object
1421+
return "object";
1422+
}
13071423

13081424
/**
13091425
* If the schema is integer and the max value is invalid (out of bound)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
openapi: 3.0.1
2+
info:
3+
version: 1.0.0
4+
title: Example
5+
license:
6+
name: MIT
7+
servers:
8+
- url: http://api.example.xyz/v1
9+
paths:
10+
/test:
11+
get:
12+
operationId: test
13+
responses:
14+
'200':
15+
description: OK
16+
content:
17+
application/json:
18+
schema:
19+
$ref: "#/components/schemas/EnumWithObjectTest"
20+
components:
21+
schemas:
22+
EnumWithObjectTest:
23+
description: Schema with oneOf containing both enums and objects
24+
oneOf:
25+
- type: string
26+
enum:
27+
- option1
28+
- option2
29+
- type: string
30+
enum:
31+
- option3
32+
- option4
33+
- $ref: "#/components/schemas/OptionObject"
34+
35+
# Test mixed cases of string enums and objects
36+
MixedTest:
37+
description: Schema with oneOf containing both enums and objects
38+
oneOf:
39+
- type: string
40+
enum:
41+
- red
42+
- blue
43+
- $ref: "#/components/schemas/ColorObject"
44+
45+
# Test with multiple string enums only
46+
StringEnumsOnly:
47+
description: Schema with oneOf containing only string enums
48+
oneOf:
49+
- type: string
50+
enum:
51+
- north
52+
- south
53+
- type: string
54+
enum:
55+
- east
56+
- west
57+
58+
# Object to be referenced in oneOf
59+
OptionObject:
60+
type: object
61+
title: CustomOption
62+
properties:
63+
code:
64+
type: string
65+
data:
66+
type: object
67+
68+
ColorObject:
69+
type: object
70+
properties:
71+
colorCode:
72+
type: string
73+
shade:
74+
type: integer

0 commit comments

Comments
 (0)