@@ -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.
0 commit comments