Skip to content

Commit 25b5c8f

Browse files
wing328pieterbos
authored andcommitted
Support normalizing anyof/oneof enum constraints to a single enum (#21917)
* Support normalizing anyof/oneof enum constraints to a single enum * Add SIMPLIFY_ONEOF_ANYOF_ENUM to the documentation * Process referenced schemas with oneof/enum as well * Implement referenced enum merging from oneof/anyof * Implement retaining the enum description as x-enum-desriptions for oneof enum * Update samples and docs with oneOf enum normalization * update samples to fix python tests * fix test file name * fix incorrect filename --------- Co-authored-by: Pieter Bos <pieter.bos@nedap.com>
1 parent 19f6522 commit 25b5c8f

File tree

17 files changed

+493
-376
lines changed

17 files changed

+493
-376
lines changed

bin/configs/python-pydantic-v1.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,5 @@ additionalProperties:
1313
nameMappings:
1414
_type: underscore_type
1515
type_: type_with_underscore
16+
openapiNormalizer:
17+
SIMPLIFY_ONEOF_ANYOF_ENUM: false

bin/configs/python.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,5 @@ nameMappings:
1313
modelNameMappings:
1414
# The OpenAPI spec ApiResponse conflicts with the internal ApiResponse
1515
ApiResponse: ModelApiResponse
16+
openapiNormalizer:
17+
SIMPLIFY_ONEOF_ANYOF_ENUM: false

docs/customization.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -564,6 +564,15 @@ Example:
564564
java -jar modules/openapi-generator-cli/target/openapi-generator-cli.jar generate -g java -i modules/openapi-generator/src/test/resources/3_0/simplifyAnyOfStringAndEnumString_test.yaml -o /tmp/java-okhttp/ --openapi-normalizer SIMPLIFY_ANYOF_STRING_AND_ENUM_STRING=true
565565
```
566566
567+
- `SIMPLIFY_ONEOF_ANYOF_ENUM`: when set to true, oneOf/anyOf with only enum sub-schemas all containing enum values will be converted to a single enum
568+
This is enabled by default
569+
570+
Example:
571+
572+
```
573+
java -jar modules/openapi-generator-cli/target/openapi-generator-cli.jar generate -g java -i modules/openapi-generator/src/test/resources/3_0/simplifyOneOfWithEnums_test.yaml -o /tmp/java-okhttp/ --openapi-normalizer SIMPLIFY_ONEOF_ANYOF_ENUM=true
574+
```
575+
567576
- `SIMPLIFY_BOOLEAN_ENUM`: when set to `true`, convert boolean enum to just enum.
568577
569578
Example:

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

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

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

210213
// rules that are default to true
211214
rules.put(SIMPLIFY_ONEOF_ANYOF, true);
212215
rules.put(SIMPLIFY_BOOLEAN_ENUM, true);
216+
rules.put(SIMPLIFY_ONEOF_ANYOF_ENUM, true);
213217

214218
processRules(inputRules);
215219

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

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

@@ -1000,6 +1006,11 @@ protected Schema normalizeOneOf(Schema schema, Set<Schema> visitedSchemas) {
10001006
}
10011007

10021008
protected Schema normalizeAnyOf(Schema schema, Set<Schema> visitedSchemas) {
1009+
//transform anyOf into enums if needed
1010+
schema = processSimplifyAnyOfEnum(schema);
1011+
if (schema.getAnyOf() == null) {
1012+
return schema;
1013+
}
10031014
for (int i = 0; i < schema.getAnyOf().size(); i++) {
10041015
// normalize anyOf sub schemas one by one
10051016
Object item = schema.getAnyOf().get(i);
@@ -1275,6 +1286,161 @@ protected Schema processSimplifyAnyOfStringAndEnumString(Schema schema) {
12751286
}
12761287

12771288

1289+
/**
1290+
* If the schema is anyOf and all sub-schemas are enums (with one or more values),
1291+
* then simplify it to a single enum schema containing all the values.
1292+
*
1293+
* @param schema Schema
1294+
* @return Schema
1295+
*/
1296+
protected Schema processSimplifyAnyOfEnum(Schema schema) {
1297+
if (!getRule(SIMPLIFY_ONEOF_ANYOF_ENUM)) {
1298+
return schema;
1299+
}
1300+
1301+
if (schema.getAnyOf() == null || schema.getAnyOf().isEmpty()) {
1302+
return schema;
1303+
}
1304+
if(schema.getOneOf() != null && !schema.getOneOf().isEmpty() ||
1305+
schema.getAllOf() != null && !schema.getAllOf().isEmpty() ||
1306+
schema.getNot() != null) {
1307+
//only convert to enum if anyOf is the only composition
1308+
return schema;
1309+
}
1310+
1311+
return simplifyComposedSchemaWithEnums(schema, schema.getAnyOf(), "anyOf");
1312+
}
1313+
1314+
/**
1315+
* If the schema is oneOf and all sub-schemas are enums (with one or more values),
1316+
* then simplify it to a single enum schema containing all the values.
1317+
*
1318+
* @param schema Schema
1319+
* @return Schema
1320+
*/
1321+
protected Schema processSimplifyOneOfEnum(Schema schema) {
1322+
if (!getRule(SIMPLIFY_ONEOF_ANYOF_ENUM)) {
1323+
return schema;
1324+
}
1325+
1326+
if (schema.getOneOf() == null || schema.getOneOf().isEmpty()) {
1327+
return schema;
1328+
}
1329+
if(schema.getAnyOf() != null && !schema.getAnyOf().isEmpty() ||
1330+
schema.getAllOf() != null && !schema.getAllOf().isEmpty() ||
1331+
schema.getNot() != null) {
1332+
//only convert to enum if oneOf is the only composition
1333+
return schema;
1334+
}
1335+
1336+
return simplifyComposedSchemaWithEnums(schema, schema.getOneOf(), "oneOf");
1337+
}
1338+
1339+
/**
1340+
* Simplifies a composed schema (oneOf/anyOf) where all sub-schemas are enums
1341+
* to a single enum schema containing all the values.
1342+
*
1343+
* @param schema Schema to modify
1344+
* @param subSchemas List of sub-schemas to check
1345+
* @param schemaType Type of composed schema ("oneOf" or "anyOf")
1346+
* @return Simplified schema
1347+
*/
1348+
protected Schema simplifyComposedSchemaWithEnums(Schema schema, List<Object> subSchemas, String composedType) {
1349+
Map<Object, String> enumValues = new LinkedHashMap<>();
1350+
1351+
if(schema.getTypes() != null && schema.getTypes().size() > 1) {
1352+
// we cannot handle enums with multiple types
1353+
return schema;
1354+
}
1355+
1356+
if(subSchemas.size() < 2) {
1357+
//do not process if there's less than 2 sub-schemas. It will be normalized later, and this prevents
1358+
//named enum schemas from being converted to inline enum schemas
1359+
return schema;
1360+
}
1361+
String schemaType = ModelUtils.getType(schema);
1362+
1363+
for (Object item : subSchemas) {
1364+
if (!(item instanceof Schema)) {
1365+
return schema;
1366+
}
1367+
1368+
Schema subSchema = ModelUtils.getReferencedSchema(openAPI, (Schema) item);
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+
// Add all enum values from this sub-schema to our collection
1389+
if(subSchema.getEnum().size() == 1) {
1390+
String description = subSchema.getTitle() == null ? "" : subSchema.getTitle();
1391+
if(subSchema.getDescription() != null) {
1392+
if(!description.isEmpty()) {
1393+
description += " - ";
1394+
}
1395+
description += subSchema.getDescription();
1396+
}
1397+
enumValues.put(subSchema.getEnum().get(0), description);
1398+
} else {
1399+
for(Object e: subSchema.getEnum()) {
1400+
enumValues.put(e, "");
1401+
}
1402+
}
1403+
1404+
}
1405+
1406+
return createSimplifiedEnumSchema(schema, enumValues, schemaType, composedType);
1407+
}
1408+
1409+
1410+
/**
1411+
* Creates a simplified enum schema from collected enum values.
1412+
*
1413+
* @param originalSchema Original schema to modify
1414+
* @param enumValues Collected enum values
1415+
* @param schemaType Consistent type across sub-schemas
1416+
* @param composedType Type of composed schema being simplified
1417+
* @return Simplified enum schema
1418+
*/
1419+
protected Schema createSimplifiedEnumSchema(Schema originalSchema, Map<Object, String> enumValues, String schemaType, String composedType) {
1420+
// Clear the composed schema type
1421+
if ("oneOf".equals(composedType)) {
1422+
originalSchema.setOneOf(null);
1423+
} else if ("anyOf".equals(composedType)) {
1424+
originalSchema.setAnyOf(null);
1425+
}
1426+
1427+
if (ModelUtils.getType(originalSchema) == null && schemaType != null) {
1428+
//if type was specified in subschemas, keep it in the main schema
1429+
ModelUtils.setType(originalSchema, schemaType);
1430+
}
1431+
1432+
originalSchema.setEnum(new ArrayList<>(enumValues.keySet()));
1433+
if(enumValues.values().stream().anyMatch(e -> !e.isEmpty())) {
1434+
//set x-enum-descriptions only if there's at least one non-empty description
1435+
originalSchema.addExtension("x-enum-descriptions", new ArrayList<>(enumValues.values()));
1436+
}
1437+
1438+
LOGGER.debug("Simplified {} with enum sub-schemas to single enum: {}", composedType, originalSchema);
1439+
1440+
return originalSchema;
1441+
}
1442+
1443+
12781444
/**
12791445
* If the schema is oneOf and the sub-schemas is null, set `nullable: true`
12801446
* 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
@@ -2179,6 +2179,22 @@ public static String getType(Schema schema) {
21792179
}
21802180
}
21812181

2182+
/**
2183+
* Set schema type.
2184+
* For 3.1 spec, set as types, for 3.0, type
2185+
*
2186+
* @param schema the schema
2187+
* @return schema type
2188+
*/
2189+
public static void setType(Schema schema, String type) {
2190+
if (schema instanceof JsonSchema) {
2191+
schema.setTypes(null);
2192+
schema.addType(type);
2193+
} else {
2194+
schema.setType(type);
2195+
}
2196+
}
2197+
21822198
/**
21832199
* Returns true if any of the common attributes of the schema (e.g. readOnly, default, maximum, etc) is defined.
21842200
*

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

Lines changed: 68 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,72 @@ 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+
assertEquals(anyOfParam.getSchema().getExtensions().get("x-enum-descriptions"), Arrays.asList("title 1", "title 2"));
190+
191+
Schema combinedRefsEnum = openAPI.getComponents().getSchemas().get("combinedRefsEnum");
192+
193+
assertEquals(anyOfParam.getSchema().getType(), "string");
194+
assertNull(combinedRefsEnum.get$ref());
195+
assertEquals(combinedRefsEnum.getEnum(), Arrays.asList("A", "B", "C", "D"));
196+
assertNull(combinedRefsEnum.getOneOf());
197+
198+
// Test with rule disabled
199+
OpenAPI openAPI2 = TestUtils.parseSpec("src/test/resources/3_0/simplifyOneOfWithEnums_test.yaml");
200+
Map<String, String> options2 = new HashMap<>();
201+
options2.put("SIMPLIFY_ONEOF_ANYOF_ENUM", "false");
202+
OpenAPINormalizer normalizer2 = new OpenAPINormalizer(openAPI2, options2);
203+
normalizer2.normalize();
204+
205+
// oneOf will be removed, as they are in this normalizer if a primitive type has a oneOf
206+
Schema colorSchema2 = openAPI2.getComponents().getSchemas().get("ColorEnum");
207+
assertNull(colorSchema2.getOneOf());
208+
assertNull(colorSchema2.getEnum());
209+
210+
//If you put string on every subscheme of oneOf, it does not remove it. This might need a fix at some other time
211+
Parameter param2 = openAPI2.getPaths().get("/test").getGet().getParameters().get(0);
212+
assertNotNull(param2.getSchema().getOneOf());
213+
assertNull(param2.getSchema().getEnum());
214+
215+
//but here it does
216+
Parameter anyOfParam2 = openAPI2.getPaths().get("/test").getGet().getParameters().get(1);
217+
assertNull(anyOfParam2.getSchema().getOneOf());
218+
assertNull(anyOfParam2.getSchema().getEnum());
219+
220+
}
221+
154222
@Test
155223
public void testOpenAPINormalizerSimplifyOneOfAnyOf() {
156224
// to test the rule SIMPLIFY_ONEOF_ANYOF

0 commit comments

Comments
 (0)