diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java index eda548cef8a..7de22a9b7e4 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java @@ -88,6 +88,10 @@ public class OpenAPINormalizer { // when set to true, boolean enum will be converted to just boolean final String SIMPLIFY_BOOLEAN_ENUM = "SIMPLIFY_BOOLEAN_ENUM"; + + // when set to true, oneOf with multiple enum schemas will be merged into a single enum schema + // even if one of them is an object + final String SIMPLIFY_ONEOF_ENUM = "SIMPLIFY_ONEOF_ENUM"; // when set to a string value, tags in all operations will be reset to the string value provided final String SET_TAGS_FOR_ALL_OPERATIONS = "SET_TAGS_FOR_ALL_OPERATIONS"; @@ -193,6 +197,7 @@ public class OpenAPINormalizer { ruleNames.add(SIMPLIFY_ANYOF_STRING_AND_ENUM_STRING); ruleNames.add(SIMPLIFY_ONEOF_ANYOF); ruleNames.add(SIMPLIFY_BOOLEAN_ENUM); + ruleNames.add(SIMPLIFY_ONEOF_ENUM); ruleNames.add(KEEP_ONLY_FIRST_TAG_IN_OPERATION); ruleNames.add(SET_TAGS_FOR_ALL_OPERATIONS); ruleNames.add(SET_TAGS_TO_OPERATIONID); @@ -211,6 +216,7 @@ public class OpenAPINormalizer { // rules that are default to true rules.put(SIMPLIFY_ONEOF_ANYOF, true); rules.put(SIMPLIFY_BOOLEAN_ENUM, true); + rules.put(SIMPLIFY_ONEOF_ENUM, true); processRules(inputRules); @@ -927,6 +933,9 @@ public class OpenAPINormalizer { // simplify first as the schema may no longer be a oneOf after processing the rule below schema = processSimplifyOneOf(schema); + + // try to merge enum schemas + schema = processSimplifyOneOfEnum(schema, visitedSchemas); // if it's still a oneOf, loop through the sub-schemas if (schema.getOneOf() != null) { @@ -1421,6 +1430,113 @@ public class OpenAPINormalizer { } } } + + /** + * If the schema is oneOf with multiple enum schemas, merge them into a single enum schema + * even if one of them is an object. + * + * @param schema Schema + * @param visitedSchemas a set of visited schemas + * @return Schema + */ + private Schema processSimplifyOneOfEnum(Schema schema, Set visitedSchemas) { + if (!getRule(SIMPLIFY_ONEOF_ENUM)) { + return schema; + } + + List oneOfSchemas = schema.getOneOf(); + if (oneOfSchemas == null || oneOfSchemas.size() <= 1) { + return schema; + } + + // Check if all schemas are either objects or have enums + boolean allEnumSchemas = true; + List allEnumValues = new ArrayList<>(); + StringSchema mergedSchema = null; + + for (Schema subSchema : oneOfSchemas) { + subSchema = ModelUtils.getReferencedSchema(openAPI, subSchema); + + if (subSchema instanceof StringSchema && ((StringSchema)subSchema).getEnum() != null) { + if (mergedSchema == null) { + // Use the first StringSchema as our template + mergedSchema = new StringSchema(); + mergedSchema.setDescription(schema.getDescription()); + mergedSchema.setExample(schema.getExample()); + mergedSchema.setExamples(schema.getExamples()); + mergedSchema.setNullable(schema.getNullable()); + mergedSchema.setDefault(schema.getDefault()); + mergedSchema.setDeprecated(schema.getDeprecated()); + } + // Add all enum values from this schema + allEnumValues.addAll(((StringSchema)subSchema).getEnum()); + } else if (ModelUtils.isObjectSchema(subSchema)) { + // If it's an object, we'll consider it valid for merging + // but we need to extract its type name as an enum value + + // Get schema name or create a placeholder + String objectEnumValue = determineObjectEnumName(subSchema); + if (objectEnumValue != null) { + if (mergedSchema == null) { + mergedSchema = new StringSchema(); + mergedSchema.setDescription(schema.getDescription()); + mergedSchema.setExample(schema.getExample()); + mergedSchema.setExamples(schema.getExamples()); + mergedSchema.setNullable(schema.getNullable()); + mergedSchema.setDefault(schema.getDefault()); + mergedSchema.setDeprecated(schema.getDeprecated()); + } + allEnumValues.add(objectEnumValue); + } else { + // If we can't determine a name, we can't merge + allEnumSchemas = false; + break; + } + } else { + // This schema is not an enum or object, can't merge + allEnumSchemas = false; + break; + } + } + + if (allEnumSchemas && mergedSchema != null && !allEnumValues.isEmpty()) { + // Remove duplicates and convert to strings + Set uniqueEnumValues = new LinkedHashSet<>(); + for (Object value : allEnumValues) { + uniqueEnumValues.add(value.toString()); + } + mergedSchema.setEnum(new ArrayList<>(uniqueEnumValues)); + + LOGGER.debug("Merged {} oneOf enum schemas into a single enum schema with values: {}", + oneOfSchemas.size(), uniqueEnumValues); + + return mergedSchema; + } + + return schema; + } + + /** + * Determines a meaningful enum value name for an object schema + * + * @param schema The object schema to determine a name for + * @return A string representing the object name, or null if can't be determined + */ + private String determineObjectEnumName(Schema schema) { + // Try to use title first + if (schema.getTitle() != null) { + return schema.getTitle(); + } + + // Try to use type or $ref name + if (schema.get$ref() != null) { + String ref = ModelUtils.getSimpleRef(schema.get$ref()); + return ref; + } + + // If no clear name, use a generic placeholder for an object + return "object"; + } /** * If the schema is integer and the max value is invalid (out of bound) diff --git a/modules/openapi-generator/src/test/resources/3_0/simplifyOneOfEnum_test.yaml b/modules/openapi-generator/src/test/resources/3_0/simplifyOneOfEnum_test.yaml new file mode 100644 index 00000000000..74bffd732b8 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/simplifyOneOfEnum_test.yaml @@ -0,0 +1,74 @@ +openapi: 3.0.1 +info: + version: 1.0.0 + title: Example + license: + name: MIT +servers: + - url: http://api.example.xyz/v1 +paths: + /test: + get: + operationId: test + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/EnumWithObjectTest" +components: + schemas: + EnumWithObjectTest: + description: Schema with oneOf containing both enums and objects + oneOf: + - type: string + enum: + - option1 + - option2 + - type: string + enum: + - option3 + - option4 + - $ref: "#/components/schemas/OptionObject" + + # Test mixed cases of string enums and objects + MixedTest: + description: Schema with oneOf containing both enums and objects + oneOf: + - type: string + enum: + - red + - blue + - $ref: "#/components/schemas/ColorObject" + + # Test with multiple string enums only + StringEnumsOnly: + description: Schema with oneOf containing only string enums + oneOf: + - type: string + enum: + - north + - south + - type: string + enum: + - east + - west + + # Object to be referenced in oneOf + OptionObject: + type: object + title: CustomOption + properties: + code: + type: string + data: + type: object + + ColorObject: + type: object + properties: + colorCode: + type: string + shade: + type: integer \ No newline at end of file