Skip to content

Simplify oneOf enums #21041

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,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";
Expand Down Expand Up @@ -159,6 +163,7 @@ public OpenAPINormalizer(OpenAPI openAPI, Map<String, String> inputRules) {
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);
Expand All @@ -176,6 +181,7 @@ public OpenAPINormalizer(OpenAPI openAPI, Map<String, String> inputRules) {
// 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);

Expand Down Expand Up @@ -787,6 +793,9 @@ private Schema normalizeAllOfWithProperties(Schema schema, Set<Schema> visitedSc
private Schema normalizeOneOf(Schema schema, Set<Schema> visitedSchemas) {
// 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) {
Expand Down Expand Up @@ -1304,6 +1313,113 @@ private void processSimplifyBooleanEnum(Schema schema) {
}
}
}

/**
* 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<Schema> visitedSchemas) {
if (!getRule(SIMPLIFY_ONEOF_ENUM)) {
return schema;
}

List<Schema> 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<Object> 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<String> 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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Loading