Skip to content
Merged
2 changes: 2 additions & 0 deletions bin/configs/python-pydantic-v1.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@ additionalProperties:
nameMappings:
_type: underscore_type
type_: type_with_underscore
openapiNormalizer:
SIMPLIFY_ONEOF_ANYOF_ENUM: false
2 changes: 2 additions & 0 deletions bin/configs/python.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@ nameMappings:
modelNameMappings:
# The OpenAPI spec ApiResponse conflicts with the internal ApiResponse
ApiResponse: ModelApiResponse
openapiNormalizer:
SIMPLIFY_ONEOF_ANYOF_ENUM: false
9 changes: 9 additions & 0 deletions docs/customization.md
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,15 @@ Example:
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
```

- `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
This is enabled by default

Example:

```
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
```

- `SIMPLIFY_BOOLEAN_ENUM`: when set to `true`, convert boolean enum to just enum.

Example:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ 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/anyOf with enum sub-schemas containing single values will be converted to a single enum
final String SIMPLIFY_ONEOF_ANYOF_ENUM = "SIMPLIFY_ONEOF_ANYOF_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";
String setTagsForAllOperations;
Expand Down Expand Up @@ -205,11 +208,12 @@ public OpenAPINormalizer(OpenAPI openAPI, Map<String, String> inputRules) {
ruleNames.add(FILTER);
ruleNames.add(SET_CONTAINER_TO_NULLABLE);
ruleNames.add(SET_PRIMITIVE_TYPES_TO_NULLABLE);

ruleNames.add(SIMPLIFY_ONEOF_ANYOF_ENUM);

// rules that are default to true
rules.put(SIMPLIFY_ONEOF_ANYOF, true);
rules.put(SIMPLIFY_BOOLEAN_ENUM, true);
rules.put(SIMPLIFY_ONEOF_ANYOF_ENUM, true);

processRules(inputRules);

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

schema = processSimplifyOneOfEnum(schema);

// simplify first as the schema may no longer be a oneOf after processing the rule below
schema = processSimplifyOneOf(schema);

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

protected Schema normalizeAnyOf(Schema schema, Set<Schema> visitedSchemas) {
//transform anyOf into enums if needed
schema = processSimplifyAnyOfEnum(schema);
if (schema.getAnyOf() == null) {
return schema;
}
for (int i = 0; i < schema.getAnyOf().size(); i++) {
// normalize anyOf sub schemas one by one
Object item = schema.getAnyOf().get(i);
Expand Down Expand Up @@ -1275,6 +1286,161 @@ protected Schema processSimplifyAnyOfStringAndEnumString(Schema schema) {
}


/**
* If the schema is anyOf and all sub-schemas are enums (with one or more values),
* then simplify it to a single enum schema containing all the values.
*
* @param schema Schema
* @return Schema
*/
protected Schema processSimplifyAnyOfEnum(Schema schema) {
if (!getRule(SIMPLIFY_ONEOF_ANYOF_ENUM)) {
return schema;
}

if (schema.getAnyOf() == null || schema.getAnyOf().isEmpty()) {
return schema;
}
if(schema.getOneOf() != null && !schema.getOneOf().isEmpty() ||
schema.getAllOf() != null && !schema.getAllOf().isEmpty() ||
schema.getNot() != null) {
//only convert to enum if anyOf is the only composition
return schema;
}

return simplifyComposedSchemaWithEnums(schema, schema.getAnyOf(), "anyOf");
}

/**
* If the schema is oneOf and all sub-schemas are enums (with one or more values),
* then simplify it to a single enum schema containing all the values.
*
* @param schema Schema
* @return Schema
*/
protected Schema processSimplifyOneOfEnum(Schema schema) {
if (!getRule(SIMPLIFY_ONEOF_ANYOF_ENUM)) {
return schema;
}

if (schema.getOneOf() == null || schema.getOneOf().isEmpty()) {
return schema;
}
if(schema.getAnyOf() != null && !schema.getAnyOf().isEmpty() ||
schema.getAllOf() != null && !schema.getAllOf().isEmpty() ||
schema.getNot() != null) {
//only convert to enum if oneOf is the only composition
return schema;
}

return simplifyComposedSchemaWithEnums(schema, schema.getOneOf(), "oneOf");
}

/**
* Simplifies a composed schema (oneOf/anyOf) where all sub-schemas are enums
* to a single enum schema containing all the values.
*
* @param schema Schema to modify
* @param subSchemas List of sub-schemas to check
* @param schemaType Type of composed schema ("oneOf" or "anyOf")
* @return Simplified schema
*/
protected Schema simplifyComposedSchemaWithEnums(Schema schema, List<Object> subSchemas, String composedType) {
Map<Object, String> enumValues = new LinkedHashMap<>();

if(schema.getTypes() != null && schema.getTypes().size() > 1) {
// we cannot handle enums with multiple types
return schema;
}

if(subSchemas.size() < 2) {
//do not process if there's less than 2 sub-schemas. It will be normalized later, and this prevents
//named enum schemas from being converted to inline enum schemas
return schema;
}
String schemaType = ModelUtils.getType(schema);

for (Object item : subSchemas) {
if (!(item instanceof Schema)) {
return schema;
}

Schema subSchema = ModelUtils.getReferencedSchema(openAPI, (Schema) item);

// Check if this sub-schema has an enum (with one or more values)
if (subSchema.getEnum() == null || subSchema.getEnum().isEmpty()) {
return schema;
}

// Ensure all sub-schemas have the same type (if type is specified)
if(subSchema.getTypes() != null && subSchema.getTypes().size() > 1) {
// we cannot handle enums with multiple types
return schema;
}
String subSchemaType = ModelUtils.getType(subSchema);
if (subSchemaType != null) {
if (schemaType == null) {
schemaType = subSchemaType;
} else if (!schemaType.equals(subSchema.getType())) {
return schema;
}
}
// Add all enum values from this sub-schema to our collection
if(subSchema.getEnum().size() == 1) {
String description = subSchema.getTitle() == null ? "" : subSchema.getTitle();
if(subSchema.getDescription() != null) {
if(!description.isEmpty()) {
description += " - ";
}
description += subSchema.getDescription();
}
enumValues.put(subSchema.getEnum().get(0), description);
} else {
for(Object e: subSchema.getEnum()) {
enumValues.put(e, "");
}
}

}

return createSimplifiedEnumSchema(schema, enumValues, schemaType, composedType);
}


/**
* Creates a simplified enum schema from collected enum values.
*
* @param originalSchema Original schema to modify
* @param enumValues Collected enum values
* @param schemaType Consistent type across sub-schemas
* @param composedType Type of composed schema being simplified
* @return Simplified enum schema
*/
protected Schema createSimplifiedEnumSchema(Schema originalSchema, Map<Object, String> enumValues, String schemaType, String composedType) {
// Clear the composed schema type
if ("oneOf".equals(composedType)) {
originalSchema.setOneOf(null);
} else if ("anyOf".equals(composedType)) {
originalSchema.setAnyOf(null);
}

if (ModelUtils.getType(originalSchema) == null && schemaType != null) {
//if type was specified in subschemas, keep it in the main schema
ModelUtils.setType(originalSchema, schemaType);
}

originalSchema.setEnum(new ArrayList<>(enumValues.keySet()));
if(enumValues.values().stream().anyMatch(e -> !e.isEmpty())) {
//set x-enum-descriptions only if there's at least one non-empty description
originalSchema.addExtension("x-enum-descriptions", new ArrayList<>(enumValues.values()));
}

LOGGER.debug("Simplified {} with enum sub-schemas to single enum: {}", composedType, originalSchema);

return originalSchema;
}


/**
* If the schema is oneOf and the sub-schemas is null, set `nullable: true`
* instead.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2179,6 +2179,22 @@ public static String getType(Schema schema) {
}
}

/**
* Set schema type.
* For 3.1 spec, set as types, for 3.0, type
*
* @param schema the schema
* @return schema type
*/
public static void setType(Schema schema, String type) {
if (schema instanceof JsonSchema) {
schema.setTypes(null);
schema.addType(type);
} else {
schema.setType(type);
}
}

/**
* Returns true if any of the common attributes of the schema (e.g. readOnly, default, maximum, etc) is defined.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.PathItem;
import io.swagger.v3.oas.models.media.*;
import io.swagger.v3.oas.models.parameters.Parameter;
import io.swagger.v3.oas.models.responses.ApiResponse;
import io.swagger.v3.oas.models.security.SecurityScheme;
import org.openapitools.codegen.utils.ModelUtils;
Expand Down Expand Up @@ -132,6 +133,7 @@ public void testOpenAPINormalizerRemoveAnyOfOneOfAndKeepPropertiesOnly() {
assertNull(schema.getAnyOf());
}


@Test
public void testOpenAPINormalizerSimplifyOneOfAnyOfStringAndEnumString() {
// to test the rule SIMPLIFY_ONEOF_ANYOF_STRING_AND_ENUM_STRING
Expand All @@ -151,6 +153,72 @@ public void testOpenAPINormalizerSimplifyOneOfAnyOfStringAndEnumString() {
assertTrue(schema3.getEnum().size() > 0);
}

@Test
public void testSimplifyOneOfAnyOfEnum() throws Exception {
// Load OpenAPI spec from external YAML file
OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_0/simplifyOneOfWithEnums_test.yaml");

// Test with rule enabled (default)
Map<String, String> options = new HashMap<>();
options.put("SIMPLIFY_ONEOF_ANYOF_ENUM", "true");
OpenAPINormalizer normalizer = new OpenAPINormalizer(openAPI, options);
normalizer.normalize();

// Verify component schema was simplified
Schema colorSchema = openAPI.getComponents().getSchemas().get("ColorEnum");
assertNull(colorSchema.getOneOf());
assertEquals(colorSchema.getType(), "string");
assertEquals(colorSchema.getEnum(), Arrays.asList("red", "green", "blue", "yellow", "purple"));

Schema statusSchema = openAPI.getComponents().getSchemas().get("StatusEnum");
assertNull(statusSchema.getOneOf());
assertEquals(statusSchema.getType(), "number");
assertEquals(statusSchema.getEnum(), Arrays.asList(1, 2, 3));

// Verify parameter schema was simplified
Parameter param = openAPI.getPaths().get("/test").getGet().getParameters().get(0);
assertNull(param.getSchema().getOneOf());
assertEquals(param.getSchema().getType(), "string");
assertEquals(param.getSchema().getEnum(), Arrays.asList("option1", "option2"));

// Verify parameter schema was simplified
Parameter anyOfParam = openAPI.getPaths().get("/test").getGet().getParameters().get(1);
assertNull(anyOfParam.getSchema().getAnyOf());
assertEquals(anyOfParam.getSchema().getType(), "string");
assertEquals(anyOfParam.getSchema().getEnum(), Arrays.asList("anyof 1", "anyof 2"));
assertEquals(anyOfParam.getSchema().getExtensions().get("x-enum-descriptions"), Arrays.asList("title 1", "title 2"));

Schema combinedRefsEnum = openAPI.getComponents().getSchemas().get("combinedRefsEnum");

assertEquals(anyOfParam.getSchema().getType(), "string");
assertNull(combinedRefsEnum.get$ref());
assertEquals(combinedRefsEnum.getEnum(), Arrays.asList("A", "B", "C", "D"));
assertNull(combinedRefsEnum.getOneOf());

// Test with rule disabled
OpenAPI openAPI2 = TestUtils.parseSpec("src/test/resources/3_0/simplifyOneOfWithEnums_test.yaml");
Map<String, String> options2 = new HashMap<>();
options2.put("SIMPLIFY_ONEOF_ANYOF_ENUM", "false");
OpenAPINormalizer normalizer2 = new OpenAPINormalizer(openAPI2, options2);
normalizer2.normalize();

// oneOf will be removed, as they are in this normalizer if a primitive type has a oneOf
Schema colorSchema2 = openAPI2.getComponents().getSchemas().get("ColorEnum");
assertNull(colorSchema2.getOneOf());
assertNull(colorSchema2.getEnum());

//If you put string on every subscheme of oneOf, it does not remove it. This might need a fix at some other time
Parameter param2 = openAPI2.getPaths().get("/test").getGet().getParameters().get(0);
assertNotNull(param2.getSchema().getOneOf());
assertNull(param2.getSchema().getEnum());

//but here it does
Parameter anyOfParam2 = openAPI2.getPaths().get("/test").getGet().getParameters().get(1);
assertNull(anyOfParam2.getSchema().getOneOf());
assertNull(anyOfParam2.getSchema().getEnum());

}

@Test
public void testOpenAPINormalizerSimplifyOneOfAnyOf() {
// to test the rule SIMPLIFY_ONEOF_ANYOF
Expand Down
Loading
Loading