Skip to content

Commit 0ae2462

Browse files
committed
Support normalizing anyof/oneof enum constraints to a single enum
1 parent f1a0935 commit 0ae2462

4 files changed

Lines changed: 270 additions & 1 deletion

File tree

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

Lines changed: 151 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,9 @@ public class OpenAPINormalizer {
8989
// when set to true, boolean enum will be converted to just boolean
9090
final String SIMPLIFY_BOOLEAN_ENUM = "SIMPLIFY_BOOLEAN_ENUM";
9191

92+
// when set to true, oneOf/anyOf with enum sub-schemas containing single values will be converted to a single enum
93+
final String SIMPLIFY_ONEOF_ANYOF_ENUM = "SIMPLIFY_ONEOF_ANYOF_ENUM";
94+
9295
// when set to a string value, tags in all operations will be reset to the string value provided
9396
final String SET_TAGS_FOR_ALL_OPERATIONS = "SET_TAGS_FOR_ALL_OPERATIONS";
9497
String setTagsForAllOperations;
@@ -206,11 +209,12 @@ public OpenAPINormalizer(OpenAPI openAPI, Map<String, String> inputRules) {
206209
ruleNames.add(FILTER);
207210
ruleNames.add(SET_CONTAINER_TO_NULLABLE);
208211
ruleNames.add(SET_PRIMITIVE_TYPES_TO_NULLABLE);
209-
212+
ruleNames.add(SIMPLIFY_ONEOF_ANYOF_ENUM);
210213

211214
// rules that are default to true
212215
rules.put(SIMPLIFY_ONEOF_ANYOF, true);
213216
rules.put(SIMPLIFY_BOOLEAN_ENUM, true);
217+
rules.put(SIMPLIFY_ONEOF_ANYOF_ENUM, true);
214218

215219
processRules(inputRules);
216220

@@ -973,6 +977,8 @@ protected Schema normalizeOneOf(Schema schema, Set<Schema> visitedSchemas) {
973977
// Remove duplicate oneOf entries
974978
ModelUtils.deduplicateOneOfSchema(schema);
975979

980+
schema = processSimplifyOneOfEnum(schema);
981+
976982
// simplify first as the schema may no longer be a oneOf after processing the rule below
977983
schema = processSimplifyOneOf(schema);
978984

@@ -1001,6 +1007,11 @@ protected Schema normalizeOneOf(Schema schema, Set<Schema> visitedSchemas) {
10011007
}
10021008

10031009
protected Schema normalizeAnyOf(Schema schema, Set<Schema> visitedSchemas) {
1010+
//transform anyOf into enums if needed
1011+
schema = processSimplifyAnyOfEnum(schema);
1012+
if (schema.getAnyOf() == null) {
1013+
return schema;
1014+
}
10041015
for (int i = 0; i < schema.getAnyOf().size(); i++) {
10051016
// normalize anyOf sub schemas one by one
10061017
Object item = schema.getAnyOf().get(i);
@@ -1276,6 +1287,145 @@ protected Schema processSimplifyAnyOfStringAndEnumString(Schema schema) {
12761287
}
12771288

12781289

1290+
/**
1291+
* If the schema is anyOf and all sub-schemas are enums (with one or more values),
1292+
* then simplify it to a single enum schema containing all the values.
1293+
*
1294+
* @param schema Schema
1295+
* @return Schema
1296+
*/
1297+
protected Schema processSimplifyAnyOfEnum(Schema schema) {
1298+
if (!getRule(SIMPLIFY_ONEOF_ANYOF_ENUM)) {
1299+
return schema;
1300+
}
1301+
1302+
if (schema.getAnyOf() == null || schema.getAnyOf().isEmpty()) {
1303+
return schema;
1304+
}
1305+
if(schema.getOneOf() != null && !schema.getOneOf().isEmpty() ||
1306+
schema.getAllOf() != null && !schema.getAllOf().isEmpty() ||
1307+
schema.getNot() != null) {
1308+
//only convert to enum if anyOf is the only composition
1309+
return schema;
1310+
}
1311+
1312+
return simplifyComposedSchemaWithEnums(schema, schema.getAnyOf(), "anyOf");
1313+
}
1314+
1315+
/**
1316+
* If the schema is oneOf and all sub-schemas are enums (with one or more values),
1317+
* then simplify it to a single enum schema containing all the values.
1318+
*
1319+
* @param schema Schema
1320+
* @return Schema
1321+
*/
1322+
protected Schema processSimplifyOneOfEnum(Schema schema) {
1323+
if (!getRule(SIMPLIFY_ONEOF_ANYOF_ENUM)) {
1324+
return schema;
1325+
}
1326+
1327+
if (schema.getOneOf() == null || schema.getOneOf().isEmpty()) {
1328+
return schema;
1329+
}
1330+
if(schema.getAnyOf() != null && !schema.getAnyOf().isEmpty() ||
1331+
schema.getAllOf() != null && !schema.getAllOf().isEmpty() ||
1332+
schema.getNot() != null) {
1333+
//only convert to enum if oneOf is the only composition
1334+
return schema;
1335+
}
1336+
1337+
return simplifyComposedSchemaWithEnums(schema, schema.getOneOf(), "oneOf");
1338+
}
1339+
1340+
/**
1341+
* Simplifies a composed schema (oneOf/anyOf) where all sub-schemas are enums
1342+
* to a single enum schema containing all the values.
1343+
*
1344+
* @param schema Schema to modify
1345+
* @param subSchemas List of sub-schemas to check
1346+
* @param schemaType Type of composed schema ("oneOf" or "anyOf")
1347+
* @return Simplified schema
1348+
*/
1349+
protected Schema simplifyComposedSchemaWithEnums(Schema schema, List<Object> subSchemas, String composedType) {
1350+
List<Object> enumValues = new ArrayList<>();
1351+
1352+
if(schema.getTypes() != null && schema.getTypes().size() > 1) {
1353+
// we cannot handle enums with multiple types
1354+
return schema;
1355+
}
1356+
String schemaType = ModelUtils.getType(schema);
1357+
1358+
for (Object item : subSchemas) {
1359+
if (!(item instanceof Schema)) {
1360+
return schema;
1361+
}
1362+
1363+
Schema subSchema = (Schema) item;
1364+
//processing references is very possible with this code (subSchema = ModelUtils.getReferencedSchema(openAPI, (Schema) item);),
1365+
// but might lead to reduced reuse in generated code
1366+
if(subSchema.get$ref() != null) {
1367+
return schema;
1368+
}
1369+
1370+
// Check if this sub-schema has an enum (with one or more values)
1371+
if (subSchema.getEnum() == null || subSchema.getEnum().isEmpty()) {
1372+
return schema;
1373+
}
1374+
1375+
// Ensure all sub-schemas have the same type (if type is specified)
1376+
if(subSchema.getTypes() != null && subSchema.getTypes().size() > 1) {
1377+
// we cannot handle enums with multiple types
1378+
return schema;
1379+
}
1380+
String subSchemaType = ModelUtils.getType(subSchema);
1381+
if (subSchemaType != null) {
1382+
if (schemaType == null) {
1383+
schemaType = subSchemaType;
1384+
} else if (!schemaType.equals(subSchema.getType())) {
1385+
return schema;
1386+
}
1387+
}
1388+
1389+
// Add all enum values from this sub-schema to our collection
1390+
enumValues.addAll(subSchema.getEnum());
1391+
}
1392+
1393+
return createSimplifiedEnumSchema(schema, enumValues, schemaType, composedType);
1394+
}
1395+
1396+
1397+
/**
1398+
* Creates a simplified enum schema from collected enum values.
1399+
*
1400+
* @param originalSchema Original schema to modify
1401+
* @param enumValues Collected enum values
1402+
* @param schemaType Consistent type across sub-schemas
1403+
* @param composedType Type of composed schema being simplified
1404+
* @return Simplified enum schema
1405+
*/
1406+
protected Schema createSimplifiedEnumSchema(Schema originalSchema, List<Object> enumValues, String schemaType, String composedType) {
1407+
// Clear the composed schema type
1408+
if ("oneOf".equals(composedType)) {
1409+
originalSchema.setOneOf(null);
1410+
} else if ("anyOf".equals(composedType)) {
1411+
originalSchema.setAnyOf(null);
1412+
}
1413+
1414+
if (ModelUtils.getType(originalSchema) == null && schemaType != null) {
1415+
//if type was specified in subschemas, keep it in the main schema
1416+
ModelUtils.setType(originalSchema, schemaType);
1417+
}
1418+
1419+
// Set the combined enum values (deduplicated to avoid duplicates)
1420+
List<Object> uniqueEnumValues = enumValues.stream().distinct().collect(Collectors.toList());
1421+
originalSchema.setEnum(uniqueEnumValues);
1422+
1423+
LOGGER.debug("Simplified {} with enum sub-schemas to single enum: {}", composedType, originalSchema);
1424+
1425+
return originalSchema;
1426+
}
1427+
1428+
12791429
/**
12801430
* If the schema is oneOf and the sub-schemas is null, set `nullable: true`
12811431
* instead.

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2164,6 +2164,22 @@ public static String getType(Schema schema) {
21642164
}
21652165
}
21662166

2167+
/**
2168+
* Set schema type.
2169+
* For 3.1 spec, set as types, for 3.0, type
2170+
*
2171+
* @param schema the schema
2172+
* @return schema type
2173+
*/
2174+
public static void setType(Schema schema, String type) {
2175+
if (schema instanceof JsonSchema) {
2176+
schema.setTypes(null);
2177+
schema.addType(type);
2178+
} else {
2179+
schema.setType(type);
2180+
}
2181+
}
2182+
21672183
/**
21682184
* Returns true if any of the common attributes of the schema (e.g. readOnly, default, maximum, etc) is defined.
21692185
*

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

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import io.swagger.v3.oas.models.OpenAPI;
2020
import io.swagger.v3.oas.models.PathItem;
2121
import io.swagger.v3.oas.models.media.*;
22+
import io.swagger.v3.oas.models.parameters.Parameter;
2223
import io.swagger.v3.oas.models.responses.ApiResponse;
2324
import io.swagger.v3.oas.models.security.SecurityScheme;
2425
import org.openapitools.codegen.utils.ModelUtils;
@@ -132,6 +133,7 @@ public void testOpenAPINormalizerRemoveAnyOfOneOfAndKeepPropertiesOnly() {
132133
assertNull(schema.getAnyOf());
133134
}
134135

136+
135137
@Test
136138
public void testOpenAPINormalizerSimplifyOneOfAnyOfStringAndEnumString() {
137139
// to test the rule SIMPLIFY_ONEOF_ANYOF_STRING_AND_ENUM_STRING
@@ -151,6 +153,63 @@ public void testOpenAPINormalizerSimplifyOneOfAnyOfStringAndEnumString() {
151153
assertTrue(schema3.getEnum().size() > 0);
152154
}
153155

156+
@Test
157+
public void testSimplifyOneOfAnyOfEnum() throws Exception {
158+
// Load OpenAPI spec from external YAML file
159+
OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_0/simplifyoneOfWithEnums_test.yaml");
160+
161+
// Test with rule enabled (default)
162+
Map<String, String> options = new HashMap<>();
163+
options.put("SIMPLIFY_ONEOF_ANYOF_ENUM", "true");
164+
OpenAPINormalizer normalizer = new OpenAPINormalizer(openAPI, options);
165+
normalizer.normalize();
166+
167+
// Verify component schema was simplified
168+
Schema colorSchema = openAPI.getComponents().getSchemas().get("ColorEnum");
169+
assertNull(colorSchema.getOneOf());
170+
assertEquals(colorSchema.getType(), "string");
171+
assertEquals(colorSchema.getEnum(), Arrays.asList("red", "green", "blue", "yellow", "purple"));
172+
173+
Schema statusSchema = openAPI.getComponents().getSchemas().get("StatusEnum");
174+
assertNull(statusSchema.getOneOf());
175+
assertEquals(statusSchema.getType(), "number");
176+
assertEquals(statusSchema.getEnum(), Arrays.asList(1, 2, 3));
177+
178+
// Verify parameter schema was simplified
179+
Parameter param = openAPI.getPaths().get("/test").getGet().getParameters().get(0);
180+
assertNull(param.getSchema().getOneOf());
181+
assertEquals(param.getSchema().getType(), "string");
182+
assertEquals(param.getSchema().getEnum(), Arrays.asList("option1", "option2"));
183+
184+
// Verify parameter schema was simplified
185+
Parameter anyOfParam = openAPI.getPaths().get("/test").getGet().getParameters().get(1);
186+
assertNull(anyOfParam.getSchema().getAnyOf());
187+
assertEquals(anyOfParam.getSchema().getType(), "string");
188+
assertEquals(anyOfParam.getSchema().getEnum(), Arrays.asList("anyof 1", "anyof 2"));
189+
190+
// Test with rule disabled
191+
OpenAPI openAPI2 = TestUtils.parseSpec("src/test/resources/3_0/simplifyoneOfWithEnums_test.yaml");
192+
Map<String, String> options2 = new HashMap<>();
193+
options2.put("SIMPLIFY_ONEOF_ANYOF_ENUM", "false");
194+
OpenAPINormalizer normalizer2 = new OpenAPINormalizer(openAPI2, options2);
195+
normalizer2.normalize();
196+
197+
// oneOf will be removed, as they are in this normalizer if a primitive type has a oneOf
198+
Schema colorSchema2 = openAPI2.getComponents().getSchemas().get("ColorEnum");
199+
assertNull(colorSchema2.getOneOf());
200+
assertNull(colorSchema2.getEnum());
201+
202+
//If you put string on every subscheme of oneOf, it does not remove it. This might need a fix at some other time
203+
Parameter param2 = openAPI2.getPaths().get("/test").getGet().getParameters().get(0);
204+
assertNotNull(param2.getSchema().getOneOf());
205+
assertNull(param2.getSchema().getEnum());
206+
207+
//but here it does
208+
Parameter anyOfParam2 = openAPI2.getPaths().get("/test").getGet().getParameters().get(1);
209+
assertNull(anyOfParam2.getSchema().getOneOf());
210+
assertNull(anyOfParam2.getSchema().getEnum());
211+
}
212+
154213
@Test
155214
public void testOpenAPINormalizerSimplifyOneOfAnyOf() {
156215
// to test the rule SIMPLIFY_ONEOF_ANYOF
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
openapi: 3.0.0
2+
info:
3+
title: Test API
4+
version: 1.0.0
5+
components:
6+
schemas:
7+
ColorEnum:
8+
type: string
9+
oneOf:
10+
- title: PrimaryColors
11+
enum: ["red", "green"]
12+
- title: SecondaryColors
13+
enum: ["blue", "yellow"]
14+
- title: purple
15+
enum: ["purple"]
16+
StatusEnum:
17+
type: number
18+
oneOf:
19+
- title: active
20+
enum: [1]
21+
- title: inactive_pending
22+
enum: [2, 3]
23+
paths:
24+
/test:
25+
get:
26+
parameters:
27+
- name: color
28+
in: query
29+
schema:
30+
oneOf:
31+
- type: string
32+
enum: ["option1"]
33+
- type: string
34+
enum: ["option2"]
35+
- name: anyOfEnum
36+
in: query
37+
schema:
38+
type: string
39+
anyOf:
40+
- enum: [ "anyof 1" ]
41+
- enum: [ "anyof 2" ]
42+
responses:
43+
'200':
44+
description: Success

0 commit comments

Comments
 (0)