From 0ae246264298dfb46b11270b907359b995ca66fc Mon Sep 17 00:00:00 2001 From: Pieter Bos Date: Thu, 4 Sep 2025 13:06:45 +0200 Subject: [PATCH 1/9] Support normalizing anyof/oneof enum constraints to a single enum --- .../codegen/OpenAPINormalizer.java | 152 +++++++++++++++++- .../codegen/utils/ModelUtils.java | 16 ++ .../codegen/OpenAPINormalizerTest.java | 59 +++++++ .../3_0/simplifyOneOfWithEnums_test.yaml | 44 +++++ 4 files changed, 270 insertions(+), 1 deletion(-) create mode 100644 modules/openapi-generator/src/test/resources/3_0/simplifyOneOfWithEnums_test.yaml 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 b8a09f99a0b5..d52d53d40d6d 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 @@ -89,6 +89,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; @@ -206,11 +209,12 @@ public OpenAPINormalizer(OpenAPI openAPI, Map 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); @@ -973,6 +977,8 @@ protected Schema normalizeOneOf(Schema schema, Set 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); @@ -1001,6 +1007,11 @@ protected Schema normalizeOneOf(Schema schema, Set visitedSchemas) { } protected Schema normalizeAnyOf(Schema schema, Set 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); @@ -1276,6 +1287,145 @@ 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 subSchemas, String composedType) { + List enumValues = new ArrayList<>(); + + if(schema.getTypes() != null && schema.getTypes().size() > 1) { + // we cannot handle enums with multiple types + return schema; + } + String schemaType = ModelUtils.getType(schema); + + for (Object item : subSchemas) { + if (!(item instanceof Schema)) { + return schema; + } + + Schema subSchema = (Schema) item; + //processing references is very possible with this code (subSchema = ModelUtils.getReferencedSchema(openAPI, (Schema) item);), + // but might lead to reduced reuse in generated code + if(subSchema.get$ref() != null) { + return schema; + } + + // 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 + enumValues.addAll(subSchema.getEnum()); + } + + 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, List 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); + } + + // Set the combined enum values (deduplicated to avoid duplicates) + List uniqueEnumValues = enumValues.stream().distinct().collect(Collectors.toList()); + originalSchema.setEnum(uniqueEnumValues); + + 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. diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java index 2c79c8a6ca83..e9802ef88960 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java @@ -2164,6 +2164,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. * diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java index 1d41104f8dc0..f41242987e67 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java @@ -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; @@ -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 @@ -151,6 +153,63 @@ 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 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")); + + // Test with rule disabled + OpenAPI openAPI2 = TestUtils.parseSpec("src/test/resources/3_0/simplifyoneOfWithEnums_test.yaml"); + Map 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 diff --git a/modules/openapi-generator/src/test/resources/3_0/simplifyOneOfWithEnums_test.yaml b/modules/openapi-generator/src/test/resources/3_0/simplifyOneOfWithEnums_test.yaml new file mode 100644 index 000000000000..291093d75503 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/simplifyOneOfWithEnums_test.yaml @@ -0,0 +1,44 @@ +openapi: 3.0.0 +info: + title: Test API + version: 1.0.0 +components: + schemas: + ColorEnum: + type: string + oneOf: + - title: PrimaryColors + enum: ["red", "green"] + - title: SecondaryColors + enum: ["blue", "yellow"] + - title: purple + enum: ["purple"] + StatusEnum: + type: number + oneOf: + - title: active + enum: [1] + - title: inactive_pending + enum: [2, 3] +paths: + /test: + get: + parameters: + - name: color + in: query + schema: + oneOf: + - type: string + enum: ["option1"] + - type: string + enum: ["option2"] + - name: anyOfEnum + in: query + schema: + type: string + anyOf: + - enum: [ "anyof 1" ] + - enum: [ "anyof 2" ] + responses: + '200': + description: Success From 9dcfc0765349a15e0fc0dded958f27fc758c55fc Mon Sep 17 00:00:00 2001 From: Pieter Bos Date: Thu, 4 Sep 2025 13:36:52 +0200 Subject: [PATCH 2/9] Add SIMPLIFY_ONEOF_ANYOF_ENUM to the documentation --- docs/customization.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/customization.md b/docs/customization.md index bf0958c8b964..acb45936fd21 100644 --- a/docs/customization.md +++ b/docs/customization.md @@ -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: From 2066ce61892152c248e33cfdef3d8f599496219e Mon Sep 17 00:00:00 2001 From: Pieter Bos Date: Fri, 5 Sep 2025 11:10:23 +0200 Subject: [PATCH 3/9] Process referenced schemas with oneof/enum as well --- .../org/openapitools/codegen/OpenAPINormalizer.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) 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 d52d53d40d6d..7136068958dc 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 @@ -1353,6 +1353,12 @@ protected Schema simplifyComposedSchemaWithEnums(Schema schema, List sub // 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) { @@ -1360,9 +1366,7 @@ protected Schema simplifyComposedSchemaWithEnums(Schema schema, List sub return schema; } - Schema subSchema = (Schema) item; - //processing references is very possible with this code (subSchema = ModelUtils.getReferencedSchema(openAPI, (Schema) item);), - // but might lead to reduced reuse in generated code + Schema subSchema = ModelUtils.getReferencedSchema(openAPI, (Schema) item); if(subSchema.get$ref() != null) { return schema; } @@ -1385,7 +1389,6 @@ protected Schema simplifyComposedSchemaWithEnums(Schema schema, List sub return schema; } } - // Add all enum values from this sub-schema to our collection enumValues.addAll(subSchema.getEnum()); } From aa58fde8f747f09e8478711bca38b192a502f0e2 Mon Sep 17 00:00:00 2001 From: Pieter Bos Date: Fri, 5 Sep 2025 11:45:34 +0200 Subject: [PATCH 4/9] Implement referenced enum merging from oneof/anyof --- .../openapitools/codegen/OpenAPINormalizer.java | 3 --- .../codegen/OpenAPINormalizerTest.java | 8 ++++++++ .../resources/3_0/simplifyOneOfWithEnums_test.yaml | 14 ++++++++++++++ 3 files changed, 22 insertions(+), 3 deletions(-) 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 7136068958dc..18d3a30ebb61 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 @@ -1367,9 +1367,6 @@ protected Schema simplifyComposedSchemaWithEnums(Schema schema, List sub } Schema subSchema = ModelUtils.getReferencedSchema(openAPI, (Schema) item); - if(subSchema.get$ref() != null) { - return schema; - } // Check if this sub-schema has an enum (with one or more values) if (subSchema.getEnum() == null || subSchema.getEnum().isEmpty()) { diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java index f41242987e67..107caf0321ec 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java @@ -187,6 +187,13 @@ public void testSimplifyOneOfAnyOfEnum() throws Exception { assertEquals(anyOfParam.getSchema().getType(), "string"); assertEquals(anyOfParam.getSchema().getEnum(), Arrays.asList("anyof 1", "anyof 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 options2 = new HashMap<>(); @@ -208,6 +215,7 @@ public void testSimplifyOneOfAnyOfEnum() throws Exception { Parameter anyOfParam2 = openAPI2.getPaths().get("/test").getGet().getParameters().get(1); assertNull(anyOfParam2.getSchema().getOneOf()); assertNull(anyOfParam2.getSchema().getEnum()); + } @Test diff --git a/modules/openapi-generator/src/test/resources/3_0/simplifyOneOfWithEnums_test.yaml b/modules/openapi-generator/src/test/resources/3_0/simplifyOneOfWithEnums_test.yaml index 291093d75503..e5b0355758e2 100644 --- a/modules/openapi-generator/src/test/resources/3_0/simplifyOneOfWithEnums_test.yaml +++ b/modules/openapi-generator/src/test/resources/3_0/simplifyOneOfWithEnums_test.yaml @@ -20,6 +20,20 @@ components: enum: [1] - title: inactive_pending enum: [2, 3] + enum1: + type: string + enum: + - A + - B + enum2: + type: string + enum: + - C + - D + combinedRefsEnum: + oneOf: + - $ref: '#/components/schemas/enum1' + - $ref: '#/components/schemas/enum2' paths: /test: get: From de90b6bf8ef9949e57b88f85091c8025f4ba3777 Mon Sep 17 00:00:00 2001 From: Pieter Bos Date: Fri, 5 Sep 2025 12:14:24 +0200 Subject: [PATCH 5/9] Implement retaining the enum description as x-enum-desriptions for oneof enum --- .../codegen/OpenAPINormalizer.java | 28 +++++++++++++++---- .../codegen/OpenAPINormalizerTest.java | 1 + .../3_0/simplifyOneOfWithEnums_test.yaml | 6 ++-- 3 files changed, 27 insertions(+), 8 deletions(-) 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 18d3a30ebb61..0041f400cb6a 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 @@ -1347,7 +1347,7 @@ protected Schema processSimplifyOneOfEnum(Schema schema) { * @return Simplified schema */ protected Schema simplifyComposedSchemaWithEnums(Schema schema, List subSchemas, String composedType) { - List enumValues = new ArrayList<>(); + Map enumValues = new LinkedHashMap<>(); if(schema.getTypes() != null && schema.getTypes().size() > 1) { // we cannot handle enums with multiple types @@ -1387,7 +1387,21 @@ protected Schema simplifyComposedSchemaWithEnums(Schema schema, List sub } } // Add all enum values from this sub-schema to our collection - enumValues.addAll(subSchema.getEnum()); + 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); @@ -1403,7 +1417,7 @@ protected Schema simplifyComposedSchemaWithEnums(Schema schema, List sub * @param composedType Type of composed schema being simplified * @return Simplified enum schema */ - protected Schema createSimplifiedEnumSchema(Schema originalSchema, List enumValues, String schemaType, String composedType) { + protected Schema createSimplifiedEnumSchema(Schema originalSchema, Map enumValues, String schemaType, String composedType) { // Clear the composed schema type if ("oneOf".equals(composedType)) { originalSchema.setOneOf(null); @@ -1416,9 +1430,11 @@ protected Schema createSimplifiedEnumSchema(Schema originalSchema, List ModelUtils.setType(originalSchema, schemaType); } - // Set the combined enum values (deduplicated to avoid duplicates) - List uniqueEnumValues = enumValues.stream().distinct().collect(Collectors.toList()); - originalSchema.setEnum(uniqueEnumValues); + 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); diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java index 107caf0321ec..c212dc9bb6d6 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java @@ -186,6 +186,7 @@ public void testSimplifyOneOfAnyOfEnum() throws Exception { 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"); diff --git a/modules/openapi-generator/src/test/resources/3_0/simplifyOneOfWithEnums_test.yaml b/modules/openapi-generator/src/test/resources/3_0/simplifyOneOfWithEnums_test.yaml index e5b0355758e2..65c5dc132fc9 100644 --- a/modules/openapi-generator/src/test/resources/3_0/simplifyOneOfWithEnums_test.yaml +++ b/modules/openapi-generator/src/test/resources/3_0/simplifyOneOfWithEnums_test.yaml @@ -51,8 +51,10 @@ paths: schema: type: string anyOf: - - enum: [ "anyof 1" ] - - enum: [ "anyof 2" ] + - title: title 1 + enum: [ "anyof 1" ] + - title: title 2 + enum: [ "anyof 2" ] responses: '200': description: Success From 91435aa5bd05a8f147392671a54ff4e0a29a09d3 Mon Sep 17 00:00:00 2001 From: Pieter Bos Date: Fri, 5 Sep 2025 12:23:56 +0200 Subject: [PATCH 6/9] Update samples and docs with oneOf enum normalization --- .../python-aiohttp/docs/OneOfEnumString.md | 22 +-- .../petstore_api/models/one_of_enum_string.py | 122 ++--------------- .../petstore_api/models/with_nested_one_of.py | 5 +- .../docs/OneOfEnumString.md | 31 ++--- .../petstore_api/models/one_of_enum_string.py | 125 ++---------------- .../petstore_api/models/with_nested_one_of.py | 5 +- .../docs/OneOfEnumString.md | 31 ++--- .../petstore_api/models/one_of_enum_string.py | 125 ++---------------- .../petstore_api/models/with_nested_one_of.py | 5 +- .../petstore/python/docs/OneOfEnumString.md | 22 +-- .../petstore_api/models/one_of_enum_string.py | 122 ++--------------- .../petstore_api/models/with_nested_one_of.py | 5 +- 12 files changed, 84 insertions(+), 536 deletions(-) diff --git a/samples/openapi3/client/petstore/python-aiohttp/docs/OneOfEnumString.md b/samples/openapi3/client/petstore/python-aiohttp/docs/OneOfEnumString.md index 1d385c092934..6f1b157c36ca 100644 --- a/samples/openapi3/client/petstore/python-aiohttp/docs/OneOfEnumString.md +++ b/samples/openapi3/client/petstore/python-aiohttp/docs/OneOfEnumString.md @@ -2,28 +2,16 @@ oneOf enum strings -## Properties +## Enum -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- +* `A` (value: `'a'`) -## Example +* `B` (value: `'b'`) -```python -from petstore_api.models.one_of_enum_string import OneOfEnumString +* `C` (value: `'c'`) -# TODO update the JSON string below -json = "{}" -# create an instance of OneOfEnumString from a JSON string -one_of_enum_string_instance = OneOfEnumString.from_json(json) -# print the JSON string representation of the object -print(OneOfEnumString.to_json()) +* `D` (value: `'d'`) -# convert the object into a dict -one_of_enum_string_dict = one_of_enum_string_instance.to_dict() -# create an instance of OneOfEnumString from a dict -one_of_enum_string_from_dict = OneOfEnumString.from_dict(one_of_enum_string_dict) -``` [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/samples/openapi3/client/petstore/python-aiohttp/petstore_api/models/one_of_enum_string.py b/samples/openapi3/client/petstore/python-aiohttp/petstore_api/models/one_of_enum_string.py index f180178737db..53101c37cc8b 100644 --- a/samples/openapi3/client/petstore/python-aiohttp/petstore_api/models/one_of_enum_string.py +++ b/samples/openapi3/client/petstore/python-aiohttp/petstore_api/models/one_of_enum_string.py @@ -14,124 +14,26 @@ from __future__ import annotations import json -import pprint -from pydantic import BaseModel, ConfigDict, Field, StrictStr, ValidationError, field_validator -from typing import Any, List, Optional -from petstore_api.models.enum_string1 import EnumString1 -from petstore_api.models.enum_string2 import EnumString2 -from pydantic import StrictStr, Field -from typing import Union, List, Set, Optional, Dict -from typing_extensions import Literal, Self +from enum import Enum +from typing_extensions import Self -ONEOFENUMSTRING_ONE_OF_SCHEMAS = ["EnumString1", "EnumString2"] -class OneOfEnumString(BaseModel): +class OneOfEnumString(str, Enum): """ oneOf enum strings """ - # data type: EnumString1 - oneof_schema_1_validator: Optional[EnumString1] = None - # data type: EnumString2 - oneof_schema_2_validator: Optional[EnumString2] = None - actual_instance: Optional[Union[EnumString1, EnumString2]] = None - one_of_schemas: Set[str] = { "EnumString1", "EnumString2" } - model_config = ConfigDict( - validate_assignment=True, - protected_namespaces=(), - ) - - - def __init__(self, *args, **kwargs) -> None: - if args: - if len(args) > 1: - raise ValueError("If a position argument is used, only 1 is allowed to set `actual_instance`") - if kwargs: - raise ValueError("If a position argument is used, keyword arguments cannot be used.") - super().__init__(actual_instance=args[0]) - else: - super().__init__(**kwargs) - - @field_validator('actual_instance') - def actual_instance_must_validate_oneof(cls, v): - instance = OneOfEnumString.model_construct() - error_messages = [] - match = 0 - # validate data type: EnumString1 - if not isinstance(v, EnumString1): - error_messages.append(f"Error! Input type `{type(v)}` is not `EnumString1`") - else: - match += 1 - # validate data type: EnumString2 - if not isinstance(v, EnumString2): - error_messages.append(f"Error! Input type `{type(v)}` is not `EnumString2`") - else: - match += 1 - if match > 1: - # more than 1 match - raise ValueError("Multiple matches found when setting `actual_instance` in OneOfEnumString with oneOf schemas: EnumString1, EnumString2. Details: " + ", ".join(error_messages)) - elif match == 0: - # no match - raise ValueError("No match found when setting `actual_instance` in OneOfEnumString with oneOf schemas: EnumString1, EnumString2. Details: " + ", ".join(error_messages)) - else: - return v - - @classmethod - def from_dict(cls, obj: Union[str, Dict[str, Any]]) -> Self: - return cls.from_json(json.dumps(obj)) + """ + allowed enum values + """ + A = 'a' + B = 'b' + C = 'c' + D = 'd' @classmethod def from_json(cls, json_str: str) -> Self: - """Returns the object represented by the json string""" - instance = cls.model_construct() - error_messages = [] - match = 0 - - # deserialize data into EnumString1 - try: - instance.actual_instance = EnumString1.from_json(json_str) - match += 1 - except (ValidationError, ValueError) as e: - error_messages.append(str(e)) - # deserialize data into EnumString2 - try: - instance.actual_instance = EnumString2.from_json(json_str) - match += 1 - except (ValidationError, ValueError) as e: - error_messages.append(str(e)) - - if match > 1: - # more than 1 match - raise ValueError("Multiple matches found when deserializing the JSON string into OneOfEnumString with oneOf schemas: EnumString1, EnumString2. Details: " + ", ".join(error_messages)) - elif match == 0: - # no match - raise ValueError("No match found when deserializing the JSON string into OneOfEnumString with oneOf schemas: EnumString1, EnumString2. Details: " + ", ".join(error_messages)) - else: - return instance - - def to_json(self) -> str: - """Returns the JSON representation of the actual instance""" - if self.actual_instance is None: - return "null" - - if hasattr(self.actual_instance, "to_json") and callable(self.actual_instance.to_json): - return self.actual_instance.to_json() - else: - return json.dumps(self.actual_instance) - - def to_dict(self) -> Optional[Union[Dict[str, Any], EnumString1, EnumString2]]: - """Returns the dict representation of the actual instance""" - if self.actual_instance is None: - return None - - if hasattr(self.actual_instance, "to_dict") and callable(self.actual_instance.to_dict): - return self.actual_instance.to_dict() - else: - # primitive type - return self.actual_instance - - def to_str(self) -> str: - """Returns the string representation of the actual instance""" - return pprint.pformat(self.model_dump()) + """Create an instance of OneOfEnumString from a JSON string""" + return cls(json.loads(json_str)) diff --git a/samples/openapi3/client/petstore/python-aiohttp/petstore_api/models/with_nested_one_of.py b/samples/openapi3/client/petstore/python-aiohttp/petstore_api/models/with_nested_one_of.py index 63ee9d8409e4..f2cd8b7a3a30 100644 --- a/samples/openapi3/client/petstore/python-aiohttp/petstore_api/models/with_nested_one_of.py +++ b/samples/openapi3/client/petstore/python-aiohttp/petstore_api/models/with_nested_one_of.py @@ -75,9 +75,6 @@ def to_dict(self) -> Dict[str, Any]: # override the default output from pydantic by calling `to_dict()` of nested_pig if self.nested_pig: _dict['nested_pig'] = self.nested_pig.to_dict() - # override the default output from pydantic by calling `to_dict()` of nested_oneof_enum_string - if self.nested_oneof_enum_string: - _dict['nested_oneof_enum_string'] = self.nested_oneof_enum_string.to_dict() return _dict @classmethod @@ -92,7 +89,7 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: _obj = cls.model_validate({ "size": obj.get("size"), "nested_pig": Pig.from_dict(obj["nested_pig"]) if obj.get("nested_pig") is not None else None, - "nested_oneof_enum_string": OneOfEnumString.from_dict(obj["nested_oneof_enum_string"]) if obj.get("nested_oneof_enum_string") is not None else None + "nested_oneof_enum_string": obj.get("nested_oneof_enum_string") }) return _obj diff --git a/samples/openapi3/client/petstore/python-pydantic-v1-aiohttp/docs/OneOfEnumString.md b/samples/openapi3/client/petstore/python-pydantic-v1-aiohttp/docs/OneOfEnumString.md index 981fae92dee3..6f1b157c36ca 100644 --- a/samples/openapi3/client/petstore/python-pydantic-v1-aiohttp/docs/OneOfEnumString.md +++ b/samples/openapi3/client/petstore/python-pydantic-v1-aiohttp/docs/OneOfEnumString.md @@ -2,27 +2,16 @@ oneOf enum strings -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- - -## Example - -```python -from petstore_api.models.one_of_enum_string import OneOfEnumString - -# TODO update the JSON string below -json = "{}" -# create an instance of OneOfEnumString from a JSON string -one_of_enum_string_instance = OneOfEnumString.from_json(json) -# print the JSON string representation of the object -print OneOfEnumString.to_json() - -# convert the object into a dict -one_of_enum_string_dict = one_of_enum_string_instance.to_dict() -# create an instance of OneOfEnumString from a dict -one_of_enum_string_from_dict = OneOfEnumString.from_dict(one_of_enum_string_dict) -``` +## Enum + +* `A` (value: `'a'`) + +* `B` (value: `'b'`) + +* `C` (value: `'c'`) + +* `D` (value: `'d'`) + [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/samples/openapi3/client/petstore/python-pydantic-v1-aiohttp/petstore_api/models/one_of_enum_string.py b/samples/openapi3/client/petstore/python-pydantic-v1-aiohttp/petstore_api/models/one_of_enum_string.py index d7ae93ccb6a8..31afa84f1e89 100644 --- a/samples/openapi3/client/petstore/python-pydantic-v1-aiohttp/petstore_api/models/one_of_enum_string.py +++ b/samples/openapi3/client/petstore/python-pydantic-v1-aiohttp/petstore_api/models/one_of_enum_string.py @@ -12,130 +12,31 @@ """ # noqa: E501 -from __future__ import annotations -from inspect import getfullargspec import json import pprint import re # noqa: F401 +from aenum import Enum, no_arg -from typing import Any, List, Optional -from pydantic import BaseModel, Field, StrictStr, ValidationError, validator -from petstore_api.models.enum_string1 import EnumString1 -from petstore_api.models.enum_string2 import EnumString2 -from typing import Union, Any, List, TYPE_CHECKING -from pydantic import StrictStr, Field -ONEOFENUMSTRING_ONE_OF_SCHEMAS = ["EnumString1", "EnumString2"] -class OneOfEnumString(BaseModel): + + +class OneOfEnumString(str, Enum): """ oneOf enum strings """ - # data type: EnumString1 - oneof_schema_1_validator: Optional[EnumString1] = None - # data type: EnumString2 - oneof_schema_2_validator: Optional[EnumString2] = None - if TYPE_CHECKING: - actual_instance: Union[EnumString1, EnumString2] - else: - actual_instance: Any - one_of_schemas: List[str] = Field(ONEOFENUMSTRING_ONE_OF_SCHEMAS, const=True) - - class Config: - validate_assignment = True - - def __init__(self, *args, **kwargs) -> None: - if args: - if len(args) > 1: - raise ValueError("If a position argument is used, only 1 is allowed to set `actual_instance`") - if kwargs: - raise ValueError("If a position argument is used, keyword arguments cannot be used.") - super().__init__(actual_instance=args[0]) - else: - super().__init__(**kwargs) - @validator('actual_instance') - def actual_instance_must_validate_oneof(cls, v): - instance = OneOfEnumString.construct() - error_messages = [] - match = 0 - # validate data type: EnumString1 - if not isinstance(v, EnumString1): - error_messages.append(f"Error! Input type `{type(v)}` is not `EnumString1`") - else: - match += 1 - # validate data type: EnumString2 - if not isinstance(v, EnumString2): - error_messages.append(f"Error! Input type `{type(v)}` is not `EnumString2`") - else: - match += 1 - if match > 1: - # more than 1 match - raise ValueError("Multiple matches found when setting `actual_instance` in OneOfEnumString with oneOf schemas: EnumString1, EnumString2. Details: " + ", ".join(error_messages)) - elif match == 0: - # no match - raise ValueError("No match found when setting `actual_instance` in OneOfEnumString with oneOf schemas: EnumString1, EnumString2. Details: " + ", ".join(error_messages)) - else: - return v - - @classmethod - def from_dict(cls, obj: dict) -> OneOfEnumString: - return cls.from_json(json.dumps(obj)) + """ + allowed enum values + """ + A = 'a' + B = 'b' + C = 'c' + D = 'd' @classmethod def from_json(cls, json_str: str) -> OneOfEnumString: - """Returns the object represented by the json string""" - instance = OneOfEnumString.construct() - error_messages = [] - match = 0 - - # deserialize data into EnumString1 - try: - instance.actual_instance = EnumString1.from_json(json_str) - match += 1 - except (ValidationError, ValueError) as e: - error_messages.append(str(e)) - # deserialize data into EnumString2 - try: - instance.actual_instance = EnumString2.from_json(json_str) - match += 1 - except (ValidationError, ValueError) as e: - error_messages.append(str(e)) - - if match > 1: - # more than 1 match - raise ValueError("Multiple matches found when deserializing the JSON string into OneOfEnumString with oneOf schemas: EnumString1, EnumString2. Details: " + ", ".join(error_messages)) - elif match == 0: - # no match - raise ValueError("No match found when deserializing the JSON string into OneOfEnumString with oneOf schemas: EnumString1, EnumString2. Details: " + ", ".join(error_messages)) - else: - return instance - - def to_json(self) -> str: - """Returns the JSON representation of the actual instance""" - if self.actual_instance is None: - return "null" - - to_json = getattr(self.actual_instance, "to_json", None) - if callable(to_json): - return self.actual_instance.to_json() - else: - return json.dumps(self.actual_instance) - - def to_dict(self) -> dict: - """Returns the dict representation of the actual instance""" - if self.actual_instance is None: - return None - - to_dict = getattr(self.actual_instance, "to_dict", None) - if callable(to_dict): - return self.actual_instance.to_dict() - else: - # primitive type - return self.actual_instance - - def to_str(self) -> str: - """Returns the string representation of the actual instance""" - return pprint.pformat(self.dict()) + """Create an instance of OneOfEnumString from a JSON string""" + return OneOfEnumString(json.loads(json_str)) diff --git a/samples/openapi3/client/petstore/python-pydantic-v1-aiohttp/petstore_api/models/with_nested_one_of.py b/samples/openapi3/client/petstore/python-pydantic-v1-aiohttp/petstore_api/models/with_nested_one_of.py index 0268402b5f4c..2fa98ce371e2 100644 --- a/samples/openapi3/client/petstore/python-pydantic-v1-aiohttp/petstore_api/models/with_nested_one_of.py +++ b/samples/openapi3/client/petstore/python-pydantic-v1-aiohttp/petstore_api/models/with_nested_one_of.py @@ -59,9 +59,6 @@ def to_dict(self): # override the default output from pydantic by calling `to_dict()` of nested_pig if self.nested_pig: _dict['nested_pig'] = self.nested_pig.to_dict() - # override the default output from pydantic by calling `to_dict()` of nested_oneof_enum_string - if self.nested_oneof_enum_string: - _dict['nested_oneof_enum_string'] = self.nested_oneof_enum_string.to_dict() return _dict @classmethod @@ -76,7 +73,7 @@ def from_dict(cls, obj: dict) -> WithNestedOneOf: _obj = WithNestedOneOf.parse_obj({ "size": obj.get("size"), "nested_pig": Pig.from_dict(obj.get("nested_pig")) if obj.get("nested_pig") is not None else None, - "nested_oneof_enum_string": OneOfEnumString.from_dict(obj.get("nested_oneof_enum_string")) if obj.get("nested_oneof_enum_string") is not None else None + "nested_oneof_enum_string": obj.get("nested_oneof_enum_string") }) return _obj diff --git a/samples/openapi3/client/petstore/python-pydantic-v1/docs/OneOfEnumString.md b/samples/openapi3/client/petstore/python-pydantic-v1/docs/OneOfEnumString.md index 981fae92dee3..6f1b157c36ca 100644 --- a/samples/openapi3/client/petstore/python-pydantic-v1/docs/OneOfEnumString.md +++ b/samples/openapi3/client/petstore/python-pydantic-v1/docs/OneOfEnumString.md @@ -2,27 +2,16 @@ oneOf enum strings -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- - -## Example - -```python -from petstore_api.models.one_of_enum_string import OneOfEnumString - -# TODO update the JSON string below -json = "{}" -# create an instance of OneOfEnumString from a JSON string -one_of_enum_string_instance = OneOfEnumString.from_json(json) -# print the JSON string representation of the object -print OneOfEnumString.to_json() - -# convert the object into a dict -one_of_enum_string_dict = one_of_enum_string_instance.to_dict() -# create an instance of OneOfEnumString from a dict -one_of_enum_string_from_dict = OneOfEnumString.from_dict(one_of_enum_string_dict) -``` +## Enum + +* `A` (value: `'a'`) + +* `B` (value: `'b'`) + +* `C` (value: `'c'`) + +* `D` (value: `'d'`) + [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/samples/openapi3/client/petstore/python-pydantic-v1/petstore_api/models/one_of_enum_string.py b/samples/openapi3/client/petstore/python-pydantic-v1/petstore_api/models/one_of_enum_string.py index d7ae93ccb6a8..31afa84f1e89 100644 --- a/samples/openapi3/client/petstore/python-pydantic-v1/petstore_api/models/one_of_enum_string.py +++ b/samples/openapi3/client/petstore/python-pydantic-v1/petstore_api/models/one_of_enum_string.py @@ -12,130 +12,31 @@ """ # noqa: E501 -from __future__ import annotations -from inspect import getfullargspec import json import pprint import re # noqa: F401 +from aenum import Enum, no_arg -from typing import Any, List, Optional -from pydantic import BaseModel, Field, StrictStr, ValidationError, validator -from petstore_api.models.enum_string1 import EnumString1 -from petstore_api.models.enum_string2 import EnumString2 -from typing import Union, Any, List, TYPE_CHECKING -from pydantic import StrictStr, Field -ONEOFENUMSTRING_ONE_OF_SCHEMAS = ["EnumString1", "EnumString2"] -class OneOfEnumString(BaseModel): + + +class OneOfEnumString(str, Enum): """ oneOf enum strings """ - # data type: EnumString1 - oneof_schema_1_validator: Optional[EnumString1] = None - # data type: EnumString2 - oneof_schema_2_validator: Optional[EnumString2] = None - if TYPE_CHECKING: - actual_instance: Union[EnumString1, EnumString2] - else: - actual_instance: Any - one_of_schemas: List[str] = Field(ONEOFENUMSTRING_ONE_OF_SCHEMAS, const=True) - - class Config: - validate_assignment = True - - def __init__(self, *args, **kwargs) -> None: - if args: - if len(args) > 1: - raise ValueError("If a position argument is used, only 1 is allowed to set `actual_instance`") - if kwargs: - raise ValueError("If a position argument is used, keyword arguments cannot be used.") - super().__init__(actual_instance=args[0]) - else: - super().__init__(**kwargs) - @validator('actual_instance') - def actual_instance_must_validate_oneof(cls, v): - instance = OneOfEnumString.construct() - error_messages = [] - match = 0 - # validate data type: EnumString1 - if not isinstance(v, EnumString1): - error_messages.append(f"Error! Input type `{type(v)}` is not `EnumString1`") - else: - match += 1 - # validate data type: EnumString2 - if not isinstance(v, EnumString2): - error_messages.append(f"Error! Input type `{type(v)}` is not `EnumString2`") - else: - match += 1 - if match > 1: - # more than 1 match - raise ValueError("Multiple matches found when setting `actual_instance` in OneOfEnumString with oneOf schemas: EnumString1, EnumString2. Details: " + ", ".join(error_messages)) - elif match == 0: - # no match - raise ValueError("No match found when setting `actual_instance` in OneOfEnumString with oneOf schemas: EnumString1, EnumString2. Details: " + ", ".join(error_messages)) - else: - return v - - @classmethod - def from_dict(cls, obj: dict) -> OneOfEnumString: - return cls.from_json(json.dumps(obj)) + """ + allowed enum values + """ + A = 'a' + B = 'b' + C = 'c' + D = 'd' @classmethod def from_json(cls, json_str: str) -> OneOfEnumString: - """Returns the object represented by the json string""" - instance = OneOfEnumString.construct() - error_messages = [] - match = 0 - - # deserialize data into EnumString1 - try: - instance.actual_instance = EnumString1.from_json(json_str) - match += 1 - except (ValidationError, ValueError) as e: - error_messages.append(str(e)) - # deserialize data into EnumString2 - try: - instance.actual_instance = EnumString2.from_json(json_str) - match += 1 - except (ValidationError, ValueError) as e: - error_messages.append(str(e)) - - if match > 1: - # more than 1 match - raise ValueError("Multiple matches found when deserializing the JSON string into OneOfEnumString with oneOf schemas: EnumString1, EnumString2. Details: " + ", ".join(error_messages)) - elif match == 0: - # no match - raise ValueError("No match found when deserializing the JSON string into OneOfEnumString with oneOf schemas: EnumString1, EnumString2. Details: " + ", ".join(error_messages)) - else: - return instance - - def to_json(self) -> str: - """Returns the JSON representation of the actual instance""" - if self.actual_instance is None: - return "null" - - to_json = getattr(self.actual_instance, "to_json", None) - if callable(to_json): - return self.actual_instance.to_json() - else: - return json.dumps(self.actual_instance) - - def to_dict(self) -> dict: - """Returns the dict representation of the actual instance""" - if self.actual_instance is None: - return None - - to_dict = getattr(self.actual_instance, "to_dict", None) - if callable(to_dict): - return self.actual_instance.to_dict() - else: - # primitive type - return self.actual_instance - - def to_str(self) -> str: - """Returns the string representation of the actual instance""" - return pprint.pformat(self.dict()) + """Create an instance of OneOfEnumString from a JSON string""" + return OneOfEnumString(json.loads(json_str)) diff --git a/samples/openapi3/client/petstore/python-pydantic-v1/petstore_api/models/with_nested_one_of.py b/samples/openapi3/client/petstore/python-pydantic-v1/petstore_api/models/with_nested_one_of.py index 8ec40c8eda1e..1f1a9bc36166 100644 --- a/samples/openapi3/client/petstore/python-pydantic-v1/petstore_api/models/with_nested_one_of.py +++ b/samples/openapi3/client/petstore/python-pydantic-v1/petstore_api/models/with_nested_one_of.py @@ -61,9 +61,6 @@ def to_dict(self): # override the default output from pydantic by calling `to_dict()` of nested_pig if self.nested_pig: _dict['nested_pig'] = self.nested_pig.to_dict() - # override the default output from pydantic by calling `to_dict()` of nested_oneof_enum_string - if self.nested_oneof_enum_string: - _dict['nested_oneof_enum_string'] = self.nested_oneof_enum_string.to_dict() # puts key-value pairs in additional_properties in the top level if self.additional_properties is not None: for _key, _value in self.additional_properties.items(): @@ -83,7 +80,7 @@ def from_dict(cls, obj: dict) -> WithNestedOneOf: _obj = WithNestedOneOf.parse_obj({ "size": obj.get("size"), "nested_pig": Pig.from_dict(obj.get("nested_pig")) if obj.get("nested_pig") is not None else None, - "nested_oneof_enum_string": OneOfEnumString.from_dict(obj.get("nested_oneof_enum_string")) if obj.get("nested_oneof_enum_string") is not None else None + "nested_oneof_enum_string": obj.get("nested_oneof_enum_string") }) # store additional fields in additional_properties for _key in obj.keys(): diff --git a/samples/openapi3/client/petstore/python/docs/OneOfEnumString.md b/samples/openapi3/client/petstore/python/docs/OneOfEnumString.md index 1d385c092934..6f1b157c36ca 100644 --- a/samples/openapi3/client/petstore/python/docs/OneOfEnumString.md +++ b/samples/openapi3/client/petstore/python/docs/OneOfEnumString.md @@ -2,28 +2,16 @@ oneOf enum strings -## Properties +## Enum -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- +* `A` (value: `'a'`) -## Example +* `B` (value: `'b'`) -```python -from petstore_api.models.one_of_enum_string import OneOfEnumString +* `C` (value: `'c'`) -# TODO update the JSON string below -json = "{}" -# create an instance of OneOfEnumString from a JSON string -one_of_enum_string_instance = OneOfEnumString.from_json(json) -# print the JSON string representation of the object -print(OneOfEnumString.to_json()) +* `D` (value: `'d'`) -# convert the object into a dict -one_of_enum_string_dict = one_of_enum_string_instance.to_dict() -# create an instance of OneOfEnumString from a dict -one_of_enum_string_from_dict = OneOfEnumString.from_dict(one_of_enum_string_dict) -``` [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/samples/openapi3/client/petstore/python/petstore_api/models/one_of_enum_string.py b/samples/openapi3/client/petstore/python/petstore_api/models/one_of_enum_string.py index f180178737db..53101c37cc8b 100644 --- a/samples/openapi3/client/petstore/python/petstore_api/models/one_of_enum_string.py +++ b/samples/openapi3/client/petstore/python/petstore_api/models/one_of_enum_string.py @@ -14,124 +14,26 @@ from __future__ import annotations import json -import pprint -from pydantic import BaseModel, ConfigDict, Field, StrictStr, ValidationError, field_validator -from typing import Any, List, Optional -from petstore_api.models.enum_string1 import EnumString1 -from petstore_api.models.enum_string2 import EnumString2 -from pydantic import StrictStr, Field -from typing import Union, List, Set, Optional, Dict -from typing_extensions import Literal, Self +from enum import Enum +from typing_extensions import Self -ONEOFENUMSTRING_ONE_OF_SCHEMAS = ["EnumString1", "EnumString2"] -class OneOfEnumString(BaseModel): +class OneOfEnumString(str, Enum): """ oneOf enum strings """ - # data type: EnumString1 - oneof_schema_1_validator: Optional[EnumString1] = None - # data type: EnumString2 - oneof_schema_2_validator: Optional[EnumString2] = None - actual_instance: Optional[Union[EnumString1, EnumString2]] = None - one_of_schemas: Set[str] = { "EnumString1", "EnumString2" } - model_config = ConfigDict( - validate_assignment=True, - protected_namespaces=(), - ) - - - def __init__(self, *args, **kwargs) -> None: - if args: - if len(args) > 1: - raise ValueError("If a position argument is used, only 1 is allowed to set `actual_instance`") - if kwargs: - raise ValueError("If a position argument is used, keyword arguments cannot be used.") - super().__init__(actual_instance=args[0]) - else: - super().__init__(**kwargs) - - @field_validator('actual_instance') - def actual_instance_must_validate_oneof(cls, v): - instance = OneOfEnumString.model_construct() - error_messages = [] - match = 0 - # validate data type: EnumString1 - if not isinstance(v, EnumString1): - error_messages.append(f"Error! Input type `{type(v)}` is not `EnumString1`") - else: - match += 1 - # validate data type: EnumString2 - if not isinstance(v, EnumString2): - error_messages.append(f"Error! Input type `{type(v)}` is not `EnumString2`") - else: - match += 1 - if match > 1: - # more than 1 match - raise ValueError("Multiple matches found when setting `actual_instance` in OneOfEnumString with oneOf schemas: EnumString1, EnumString2. Details: " + ", ".join(error_messages)) - elif match == 0: - # no match - raise ValueError("No match found when setting `actual_instance` in OneOfEnumString with oneOf schemas: EnumString1, EnumString2. Details: " + ", ".join(error_messages)) - else: - return v - - @classmethod - def from_dict(cls, obj: Union[str, Dict[str, Any]]) -> Self: - return cls.from_json(json.dumps(obj)) + """ + allowed enum values + """ + A = 'a' + B = 'b' + C = 'c' + D = 'd' @classmethod def from_json(cls, json_str: str) -> Self: - """Returns the object represented by the json string""" - instance = cls.model_construct() - error_messages = [] - match = 0 - - # deserialize data into EnumString1 - try: - instance.actual_instance = EnumString1.from_json(json_str) - match += 1 - except (ValidationError, ValueError) as e: - error_messages.append(str(e)) - # deserialize data into EnumString2 - try: - instance.actual_instance = EnumString2.from_json(json_str) - match += 1 - except (ValidationError, ValueError) as e: - error_messages.append(str(e)) - - if match > 1: - # more than 1 match - raise ValueError("Multiple matches found when deserializing the JSON string into OneOfEnumString with oneOf schemas: EnumString1, EnumString2. Details: " + ", ".join(error_messages)) - elif match == 0: - # no match - raise ValueError("No match found when deserializing the JSON string into OneOfEnumString with oneOf schemas: EnumString1, EnumString2. Details: " + ", ".join(error_messages)) - else: - return instance - - def to_json(self) -> str: - """Returns the JSON representation of the actual instance""" - if self.actual_instance is None: - return "null" - - if hasattr(self.actual_instance, "to_json") and callable(self.actual_instance.to_json): - return self.actual_instance.to_json() - else: - return json.dumps(self.actual_instance) - - def to_dict(self) -> Optional[Union[Dict[str, Any], EnumString1, EnumString2]]: - """Returns the dict representation of the actual instance""" - if self.actual_instance is None: - return None - - if hasattr(self.actual_instance, "to_dict") and callable(self.actual_instance.to_dict): - return self.actual_instance.to_dict() - else: - # primitive type - return self.actual_instance - - def to_str(self) -> str: - """Returns the string representation of the actual instance""" - return pprint.pformat(self.model_dump()) + """Create an instance of OneOfEnumString from a JSON string""" + return cls(json.loads(json_str)) diff --git a/samples/openapi3/client/petstore/python/petstore_api/models/with_nested_one_of.py b/samples/openapi3/client/petstore/python/petstore_api/models/with_nested_one_of.py index eb7e90879e90..eaee08c294c6 100644 --- a/samples/openapi3/client/petstore/python/petstore_api/models/with_nested_one_of.py +++ b/samples/openapi3/client/petstore/python/petstore_api/models/with_nested_one_of.py @@ -78,9 +78,6 @@ def to_dict(self) -> Dict[str, Any]: # override the default output from pydantic by calling `to_dict()` of nested_pig if self.nested_pig: _dict['nested_pig'] = self.nested_pig.to_dict() - # override the default output from pydantic by calling `to_dict()` of nested_oneof_enum_string - if self.nested_oneof_enum_string: - _dict['nested_oneof_enum_string'] = self.nested_oneof_enum_string.to_dict() # puts key-value pairs in additional_properties in the top level if self.additional_properties is not None: for _key, _value in self.additional_properties.items(): @@ -100,7 +97,7 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: _obj = cls.model_validate({ "size": obj.get("size"), "nested_pig": Pig.from_dict(obj["nested_pig"]) if obj.get("nested_pig") is not None else None, - "nested_oneof_enum_string": OneOfEnumString.from_dict(obj["nested_oneof_enum_string"]) if obj.get("nested_oneof_enum_string") is not None else None + "nested_oneof_enum_string": obj.get("nested_oneof_enum_string") }) # store additional fields in additional_properties for _key in obj.keys(): From f54b2aa51ef3ac043fcaeaa8ba546b73cdc034a0 Mon Sep 17 00:00:00 2001 From: William Cheng Date: Sun, 7 Sep 2025 16:36:07 +0800 Subject: [PATCH 7/9] update samples to fix python tests --- bin/configs/python-pydantic-v1.yaml | 2 + bin/configs/python.yaml | 2 + .../latest/ComposedEnum/api/openapi.yaml | 9 +- .../Org.OpenAPITools/Client/ClientUtils.cs | 2 + .../Client/HostConfiguration.cs | 1 + .../src/Org.OpenAPITools/Model/AreaCode.cs | 222 ++++++++++-------- .../docs/OneOfEnumString.md | 31 ++- .../petstore_api/models/one_of_enum_string.py | 125 +++++++++- .../petstore_api/models/with_nested_one_of.py | 5 +- .../petstore/python/docs/OneOfEnumString.md | 22 +- .../petstore_api/models/one_of_enum_string.py | 122 +++++++++- .../petstore_api/models/with_nested_one_of.py | 5 +- 12 files changed, 399 insertions(+), 149 deletions(-) diff --git a/bin/configs/python-pydantic-v1.yaml b/bin/configs/python-pydantic-v1.yaml index 106565672653..e5530ff42019 100644 --- a/bin/configs/python-pydantic-v1.yaml +++ b/bin/configs/python-pydantic-v1.yaml @@ -13,3 +13,5 @@ additionalProperties: nameMappings: _type: underscore_type type_: type_with_underscore +openapiNormalizer: + SIMPLIFY_ONEOF_ANYOF_ENUM: false diff --git a/bin/configs/python.yaml b/bin/configs/python.yaml index 09727cdfab9a..606ef8c77423 100644 --- a/bin/configs/python.yaml +++ b/bin/configs/python.yaml @@ -13,3 +13,5 @@ nameMappings: modelNameMappings: # The OpenAPI spec ApiResponse conflicts with the internal ApiResponse ApiResponse: ModelApiResponse +openapiNormalizer: + SIMPLIFY_ONEOF_ANYOF_ENUM: false diff --git a/samples/client/petstore/csharp/generichost/latest/ComposedEnum/api/openapi.yaml b/samples/client/petstore/csharp/generichost/latest/ComposedEnum/api/openapi.yaml index 1f84136ce5b7..69a25ff68141 100644 --- a/samples/client/petstore/csharp/generichost/latest/ComposedEnum/api/openapi.yaml +++ b/samples/client/petstore/csharp/generichost/latest/ComposedEnum/api/openapi.yaml @@ -8,9 +8,12 @@ servers: components: schemas: AreaCode: - oneOf: - - $ref: "#/components/schemas/StateTerritoryCode" - - $ref: "#/components/schemas/MarineAreaCode" + enum: + - AL + - AK + - AM + - AN + type: string StateTerritoryCode: enum: - AL diff --git a/samples/client/petstore/csharp/generichost/latest/ComposedEnum/src/Org.OpenAPITools/Client/ClientUtils.cs b/samples/client/petstore/csharp/generichost/latest/ComposedEnum/src/Org.OpenAPITools/Client/ClientUtils.cs index 90d10488d811..c72d032fc8f1 100644 --- a/samples/client/petstore/csharp/generichost/latest/ComposedEnum/src/Org.OpenAPITools/Client/ClientUtils.cs +++ b/samples/client/petstore/csharp/generichost/latest/ComposedEnum/src/Org.OpenAPITools/Client/ClientUtils.cs @@ -111,6 +111,8 @@ public static bool TryDeserialize(ref Utf8JsonReader reader, JsonSerializerOp return boolean ? "true" : "false"; + if (obj is AreaCode areaCode) + return AreaCodeValueConverter.ToJsonValue(areaCode); if (obj is MarineAreaCode marineAreaCode) return MarineAreaCodeValueConverter.ToJsonValue(marineAreaCode); if (obj is StateTerritoryCode stateTerritoryCode) diff --git a/samples/client/petstore/csharp/generichost/latest/ComposedEnum/src/Org.OpenAPITools/Client/HostConfiguration.cs b/samples/client/petstore/csharp/generichost/latest/ComposedEnum/src/Org.OpenAPITools/Client/HostConfiguration.cs index 1d16263bfe42..57cd55318ffc 100644 --- a/samples/client/petstore/csharp/generichost/latest/ComposedEnum/src/Org.OpenAPITools/Client/HostConfiguration.cs +++ b/samples/client/petstore/csharp/generichost/latest/ComposedEnum/src/Org.OpenAPITools/Client/HostConfiguration.cs @@ -44,6 +44,7 @@ public HostConfiguration(IServiceCollection services) _jsonOptions.Converters.Add(new DateOnlyJsonConverter()); _jsonOptions.Converters.Add(new DateOnlyNullableJsonConverter()); _jsonOptions.Converters.Add(new AreaCodeJsonConverter()); + _jsonOptions.Converters.Add(new AreaCodeNullableJsonConverter()); _jsonOptions.Converters.Add(new MarineAreaCodeJsonConverter()); _jsonOptions.Converters.Add(new MarineAreaCodeNullableJsonConverter()); _jsonOptions.Converters.Add(new StateTerritoryCodeJsonConverter()); diff --git a/samples/client/petstore/csharp/generichost/latest/ComposedEnum/src/Org.OpenAPITools/Model/AreaCode.cs b/samples/client/petstore/csharp/generichost/latest/ComposedEnum/src/Org.OpenAPITools/Model/AreaCode.cs index 8d0604afacd4..12a51fd09fa5 100644 --- a/samples/client/petstore/csharp/generichost/latest/ComposedEnum/src/Org.OpenAPITools/Model/AreaCode.cs +++ b/samples/client/petstore/csharp/generichost/latest/ComposedEnum/src/Org.OpenAPITools/Model/AreaCode.cs @@ -26,164 +26,178 @@ namespace Org.OpenAPITools.Model { /// - /// AreaCode + /// Defines AreaCode /// - public partial class AreaCode : IValidatableObject + public enum AreaCode { /// - /// Initializes a new instance of the class. + /// Enum AL for value: AL /// - /// - internal AreaCode(StateTerritoryCode stateTerritoryCode) - { - StateTerritoryCode = stateTerritoryCode; - OnCreated(); - } + AL = 1, /// - /// Initializes a new instance of the class. + /// Enum AK for value: AK /// - /// - internal AreaCode(MarineAreaCode marineAreaCode) - { - MarineAreaCode = marineAreaCode; - OnCreated(); - } + AK = 2, - partial void OnCreated(); + /// + /// Enum AM for value: AM + /// + AM = 3, /// - /// Gets or Sets StateTerritoryCode + /// Enum AN for value: AN /// - public StateTerritoryCode? StateTerritoryCode { get; set; } + AN = 4 + } + /// + /// Converts to and from the JSON value + /// + public static class AreaCodeValueConverter + { /// - /// Gets or Sets MarineAreaCode + /// Parses a given value to /// - public MarineAreaCode? MarineAreaCode { get; set; } + /// + /// + public static AreaCode FromString(string value) + { + if (value.Equals("AL")) + return AreaCode.AL; + + if (value.Equals("AK")) + return AreaCode.AK; + + if (value.Equals("AM")) + return AreaCode.AM; + + if (value.Equals("AN")) + return AreaCode.AN; + + throw new NotImplementedException($"Could not convert value to type AreaCode: '{value}'"); + } /// - /// Returns the string presentation of the object + /// Parses a given value to /// - /// String presentation of the object - public override string ToString() + /// + /// + public static AreaCode? FromStringOrDefault(string value) { - StringBuilder sb = new StringBuilder(); - sb.Append("class AreaCode {\n"); - sb.Append("}\n"); - return sb.ToString(); + if (value.Equals("AL")) + return AreaCode.AL; + + if (value.Equals("AK")) + return AreaCode.AK; + + if (value.Equals("AM")) + return AreaCode.AM; + + if (value.Equals("AN")) + return AreaCode.AN; + + return null; } /// - /// To validate all properties of the instance + /// Converts the to the json value /// - /// Validation context - /// Validation Result - IEnumerable IValidatableObject.Validate(ValidationContext validationContext) + /// + /// + /// + public static string ToJsonValue(AreaCode value) { - yield break; + if (value == AreaCode.AL) + return "AL"; + + if (value == AreaCode.AK) + return "AK"; + + if (value == AreaCode.AM) + return "AM"; + + if (value == AreaCode.AN) + return "AN"; + + throw new NotImplementedException($"Value could not be handled: '{value}'"); } } /// - /// A Json converter for type + /// A Json converter for type /// + /// public class AreaCodeJsonConverter : JsonConverter { /// - /// Deserializes json to + /// Returns a from the Json object /// - /// + /// /// - /// + /// /// - /// - public override AreaCode Read(ref Utf8JsonReader utf8JsonReader, Type typeToConvert, JsonSerializerOptions jsonSerializerOptions) + public override AreaCode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - int currentDepth = utf8JsonReader.CurrentDepth; + string? rawValue = reader.GetString(); - if (utf8JsonReader.TokenType != JsonTokenType.StartObject && utf8JsonReader.TokenType != JsonTokenType.StartArray) - throw new JsonException(); + AreaCode? result = rawValue == null + ? null + : AreaCodeValueConverter.FromStringOrDefault(rawValue); - JsonTokenType startingTokenType = utf8JsonReader.TokenType; - - StateTerritoryCode? stateTerritoryCode = default; - MarineAreaCode? marineAreaCode = default; - - Utf8JsonReader utf8JsonReaderOneOf = utf8JsonReader; - while (utf8JsonReaderOneOf.Read()) - { - if (startingTokenType == JsonTokenType.StartObject && utf8JsonReaderOneOf.TokenType == JsonTokenType.EndObject && currentDepth == utf8JsonReaderOneOf.CurrentDepth) - break; - - if (startingTokenType == JsonTokenType.StartArray && utf8JsonReaderOneOf.TokenType == JsonTokenType.EndArray && currentDepth == utf8JsonReaderOneOf.CurrentDepth) - break; - - if (utf8JsonReaderOneOf.TokenType == JsonTokenType.PropertyName && currentDepth == utf8JsonReaderOneOf.CurrentDepth - 1) - { - Utf8JsonReader utf8JsonReaderStateTerritoryCode = utf8JsonReader; - ClientUtils.TryDeserialize(ref utf8JsonReaderStateTerritoryCode, jsonSerializerOptions, out stateTerritoryCode); - - Utf8JsonReader utf8JsonReaderMarineAreaCode = utf8JsonReader; - ClientUtils.TryDeserialize(ref utf8JsonReaderMarineAreaCode, jsonSerializerOptions, out marineAreaCode); - } - } - - while (utf8JsonReader.Read()) - { - if (startingTokenType == JsonTokenType.StartObject && utf8JsonReader.TokenType == JsonTokenType.EndObject && currentDepth == utf8JsonReader.CurrentDepth) - break; - - if (startingTokenType == JsonTokenType.StartArray && utf8JsonReader.TokenType == JsonTokenType.EndArray && currentDepth == utf8JsonReader.CurrentDepth) - break; - - if (utf8JsonReader.TokenType == JsonTokenType.PropertyName && currentDepth == utf8JsonReader.CurrentDepth - 1) - { - string? localVarJsonPropertyName = utf8JsonReader.GetString(); - utf8JsonReader.Read(); - - switch (localVarJsonPropertyName) - { - default: - break; - } - } - } - - if (stateTerritoryCode != null) - return new AreaCode(stateTerritoryCode.Value); - - if (marineAreaCode != null) - return new AreaCode(marineAreaCode.Value); + if (result != null) + return result.Value; throw new JsonException(); } /// - /// Serializes a + /// Writes the AreaCode to the json writer /// /// /// - /// - /// - public override void Write(Utf8JsonWriter writer, AreaCode areaCode, JsonSerializerOptions jsonSerializerOptions) + /// + public override void Write(Utf8JsonWriter writer, AreaCode areaCode, JsonSerializerOptions options) + { + writer.WriteStringValue(AreaCodeValueConverter.ToJsonValue(areaCode).ToString()); + } + } + + /// + /// A Json converter for type + /// + public class AreaCodeNullableJsonConverter : JsonConverter + { + /// + /// Returns a AreaCode from the Json object + /// + /// + /// + /// + /// + public override AreaCode? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - writer.WriteStartObject(); + string? rawValue = reader.GetString(); + + AreaCode? result = rawValue == null + ? null + : AreaCodeValueConverter.FromStringOrDefault(rawValue); + + if (result != null) + return result.Value; - WriteProperties(writer, areaCode, jsonSerializerOptions); - writer.WriteEndObject(); + throw new JsonException(); } /// - /// Serializes the properties of + /// Writes the AreaCode to the json writer /// /// /// - /// - /// - public void WriteProperties(Utf8JsonWriter writer, AreaCode areaCode, JsonSerializerOptions jsonSerializerOptions) + /// + public override void Write(Utf8JsonWriter writer, AreaCode? areaCode, JsonSerializerOptions options) { - + writer.WriteStringValue(areaCode.HasValue ? AreaCodeValueConverter.ToJsonValue(areaCode.Value).ToString() : "null"); } } } diff --git a/samples/openapi3/client/petstore/python-pydantic-v1/docs/OneOfEnumString.md b/samples/openapi3/client/petstore/python-pydantic-v1/docs/OneOfEnumString.md index 6f1b157c36ca..981fae92dee3 100644 --- a/samples/openapi3/client/petstore/python-pydantic-v1/docs/OneOfEnumString.md +++ b/samples/openapi3/client/petstore/python-pydantic-v1/docs/OneOfEnumString.md @@ -2,16 +2,27 @@ oneOf enum strings -## Enum - -* `A` (value: `'a'`) - -* `B` (value: `'b'`) - -* `C` (value: `'c'`) - -* `D` (value: `'d'`) - +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- + +## Example + +```python +from petstore_api.models.one_of_enum_string import OneOfEnumString + +# TODO update the JSON string below +json = "{}" +# create an instance of OneOfEnumString from a JSON string +one_of_enum_string_instance = OneOfEnumString.from_json(json) +# print the JSON string representation of the object +print OneOfEnumString.to_json() + +# convert the object into a dict +one_of_enum_string_dict = one_of_enum_string_instance.to_dict() +# create an instance of OneOfEnumString from a dict +one_of_enum_string_from_dict = OneOfEnumString.from_dict(one_of_enum_string_dict) +``` [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/samples/openapi3/client/petstore/python-pydantic-v1/petstore_api/models/one_of_enum_string.py b/samples/openapi3/client/petstore/python-pydantic-v1/petstore_api/models/one_of_enum_string.py index 31afa84f1e89..d7ae93ccb6a8 100644 --- a/samples/openapi3/client/petstore/python-pydantic-v1/petstore_api/models/one_of_enum_string.py +++ b/samples/openapi3/client/petstore/python-pydantic-v1/petstore_api/models/one_of_enum_string.py @@ -12,31 +12,130 @@ """ # noqa: E501 +from __future__ import annotations +from inspect import getfullargspec import json import pprint import re # noqa: F401 -from aenum import Enum, no_arg +from typing import Any, List, Optional +from pydantic import BaseModel, Field, StrictStr, ValidationError, validator +from petstore_api.models.enum_string1 import EnumString1 +from petstore_api.models.enum_string2 import EnumString2 +from typing import Union, Any, List, TYPE_CHECKING +from pydantic import StrictStr, Field +ONEOFENUMSTRING_ONE_OF_SCHEMAS = ["EnumString1", "EnumString2"] - - -class OneOfEnumString(str, Enum): +class OneOfEnumString(BaseModel): """ oneOf enum strings """ + # data type: EnumString1 + oneof_schema_1_validator: Optional[EnumString1] = None + # data type: EnumString2 + oneof_schema_2_validator: Optional[EnumString2] = None + if TYPE_CHECKING: + actual_instance: Union[EnumString1, EnumString2] + else: + actual_instance: Any + one_of_schemas: List[str] = Field(ONEOFENUMSTRING_ONE_OF_SCHEMAS, const=True) - """ - allowed enum values - """ - A = 'a' - B = 'b' - C = 'c' - D = 'd' + class Config: + validate_assignment = True + + def __init__(self, *args, **kwargs) -> None: + if args: + if len(args) > 1: + raise ValueError("If a position argument is used, only 1 is allowed to set `actual_instance`") + if kwargs: + raise ValueError("If a position argument is used, keyword arguments cannot be used.") + super().__init__(actual_instance=args[0]) + else: + super().__init__(**kwargs) + + @validator('actual_instance') + def actual_instance_must_validate_oneof(cls, v): + instance = OneOfEnumString.construct() + error_messages = [] + match = 0 + # validate data type: EnumString1 + if not isinstance(v, EnumString1): + error_messages.append(f"Error! Input type `{type(v)}` is not `EnumString1`") + else: + match += 1 + # validate data type: EnumString2 + if not isinstance(v, EnumString2): + error_messages.append(f"Error! Input type `{type(v)}` is not `EnumString2`") + else: + match += 1 + if match > 1: + # more than 1 match + raise ValueError("Multiple matches found when setting `actual_instance` in OneOfEnumString with oneOf schemas: EnumString1, EnumString2. Details: " + ", ".join(error_messages)) + elif match == 0: + # no match + raise ValueError("No match found when setting `actual_instance` in OneOfEnumString with oneOf schemas: EnumString1, EnumString2. Details: " + ", ".join(error_messages)) + else: + return v + + @classmethod + def from_dict(cls, obj: dict) -> OneOfEnumString: + return cls.from_json(json.dumps(obj)) @classmethod def from_json(cls, json_str: str) -> OneOfEnumString: - """Create an instance of OneOfEnumString from a JSON string""" - return OneOfEnumString(json.loads(json_str)) + """Returns the object represented by the json string""" + instance = OneOfEnumString.construct() + error_messages = [] + match = 0 + + # deserialize data into EnumString1 + try: + instance.actual_instance = EnumString1.from_json(json_str) + match += 1 + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + # deserialize data into EnumString2 + try: + instance.actual_instance = EnumString2.from_json(json_str) + match += 1 + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + + if match > 1: + # more than 1 match + raise ValueError("Multiple matches found when deserializing the JSON string into OneOfEnumString with oneOf schemas: EnumString1, EnumString2. Details: " + ", ".join(error_messages)) + elif match == 0: + # no match + raise ValueError("No match found when deserializing the JSON string into OneOfEnumString with oneOf schemas: EnumString1, EnumString2. Details: " + ", ".join(error_messages)) + else: + return instance + + def to_json(self) -> str: + """Returns the JSON representation of the actual instance""" + if self.actual_instance is None: + return "null" + + to_json = getattr(self.actual_instance, "to_json", None) + if callable(to_json): + return self.actual_instance.to_json() + else: + return json.dumps(self.actual_instance) + + def to_dict(self) -> dict: + """Returns the dict representation of the actual instance""" + if self.actual_instance is None: + return None + + to_dict = getattr(self.actual_instance, "to_dict", None) + if callable(to_dict): + return self.actual_instance.to_dict() + else: + # primitive type + return self.actual_instance + + def to_str(self) -> str: + """Returns the string representation of the actual instance""" + return pprint.pformat(self.dict()) diff --git a/samples/openapi3/client/petstore/python-pydantic-v1/petstore_api/models/with_nested_one_of.py b/samples/openapi3/client/petstore/python-pydantic-v1/petstore_api/models/with_nested_one_of.py index 1f1a9bc36166..8ec40c8eda1e 100644 --- a/samples/openapi3/client/petstore/python-pydantic-v1/petstore_api/models/with_nested_one_of.py +++ b/samples/openapi3/client/petstore/python-pydantic-v1/petstore_api/models/with_nested_one_of.py @@ -61,6 +61,9 @@ def to_dict(self): # override the default output from pydantic by calling `to_dict()` of nested_pig if self.nested_pig: _dict['nested_pig'] = self.nested_pig.to_dict() + # override the default output from pydantic by calling `to_dict()` of nested_oneof_enum_string + if self.nested_oneof_enum_string: + _dict['nested_oneof_enum_string'] = self.nested_oneof_enum_string.to_dict() # puts key-value pairs in additional_properties in the top level if self.additional_properties is not None: for _key, _value in self.additional_properties.items(): @@ -80,7 +83,7 @@ def from_dict(cls, obj: dict) -> WithNestedOneOf: _obj = WithNestedOneOf.parse_obj({ "size": obj.get("size"), "nested_pig": Pig.from_dict(obj.get("nested_pig")) if obj.get("nested_pig") is not None else None, - "nested_oneof_enum_string": obj.get("nested_oneof_enum_string") + "nested_oneof_enum_string": OneOfEnumString.from_dict(obj.get("nested_oneof_enum_string")) if obj.get("nested_oneof_enum_string") is not None else None }) # store additional fields in additional_properties for _key in obj.keys(): diff --git a/samples/openapi3/client/petstore/python/docs/OneOfEnumString.md b/samples/openapi3/client/petstore/python/docs/OneOfEnumString.md index 6f1b157c36ca..1d385c092934 100644 --- a/samples/openapi3/client/petstore/python/docs/OneOfEnumString.md +++ b/samples/openapi3/client/petstore/python/docs/OneOfEnumString.md @@ -2,16 +2,28 @@ oneOf enum strings -## Enum +## Properties -* `A` (value: `'a'`) +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- -* `B` (value: `'b'`) +## Example -* `C` (value: `'c'`) +```python +from petstore_api.models.one_of_enum_string import OneOfEnumString -* `D` (value: `'d'`) +# TODO update the JSON string below +json = "{}" +# create an instance of OneOfEnumString from a JSON string +one_of_enum_string_instance = OneOfEnumString.from_json(json) +# print the JSON string representation of the object +print(OneOfEnumString.to_json()) +# convert the object into a dict +one_of_enum_string_dict = one_of_enum_string_instance.to_dict() +# create an instance of OneOfEnumString from a dict +one_of_enum_string_from_dict = OneOfEnumString.from_dict(one_of_enum_string_dict) +``` [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/samples/openapi3/client/petstore/python/petstore_api/models/one_of_enum_string.py b/samples/openapi3/client/petstore/python/petstore_api/models/one_of_enum_string.py index 53101c37cc8b..f180178737db 100644 --- a/samples/openapi3/client/petstore/python/petstore_api/models/one_of_enum_string.py +++ b/samples/openapi3/client/petstore/python/petstore_api/models/one_of_enum_string.py @@ -14,26 +14,124 @@ from __future__ import annotations import json -from enum import Enum -from typing_extensions import Self +import pprint +from pydantic import BaseModel, ConfigDict, Field, StrictStr, ValidationError, field_validator +from typing import Any, List, Optional +from petstore_api.models.enum_string1 import EnumString1 +from petstore_api.models.enum_string2 import EnumString2 +from pydantic import StrictStr, Field +from typing import Union, List, Set, Optional, Dict +from typing_extensions import Literal, Self +ONEOFENUMSTRING_ONE_OF_SCHEMAS = ["EnumString1", "EnumString2"] -class OneOfEnumString(str, Enum): +class OneOfEnumString(BaseModel): """ oneOf enum strings """ + # data type: EnumString1 + oneof_schema_1_validator: Optional[EnumString1] = None + # data type: EnumString2 + oneof_schema_2_validator: Optional[EnumString2] = None + actual_instance: Optional[Union[EnumString1, EnumString2]] = None + one_of_schemas: Set[str] = { "EnumString1", "EnumString2" } - """ - allowed enum values - """ - A = 'a' - B = 'b' - C = 'c' - D = 'd' + model_config = ConfigDict( + validate_assignment=True, + protected_namespaces=(), + ) + + + def __init__(self, *args, **kwargs) -> None: + if args: + if len(args) > 1: + raise ValueError("If a position argument is used, only 1 is allowed to set `actual_instance`") + if kwargs: + raise ValueError("If a position argument is used, keyword arguments cannot be used.") + super().__init__(actual_instance=args[0]) + else: + super().__init__(**kwargs) + + @field_validator('actual_instance') + def actual_instance_must_validate_oneof(cls, v): + instance = OneOfEnumString.model_construct() + error_messages = [] + match = 0 + # validate data type: EnumString1 + if not isinstance(v, EnumString1): + error_messages.append(f"Error! Input type `{type(v)}` is not `EnumString1`") + else: + match += 1 + # validate data type: EnumString2 + if not isinstance(v, EnumString2): + error_messages.append(f"Error! Input type `{type(v)}` is not `EnumString2`") + else: + match += 1 + if match > 1: + # more than 1 match + raise ValueError("Multiple matches found when setting `actual_instance` in OneOfEnumString with oneOf schemas: EnumString1, EnumString2. Details: " + ", ".join(error_messages)) + elif match == 0: + # no match + raise ValueError("No match found when setting `actual_instance` in OneOfEnumString with oneOf schemas: EnumString1, EnumString2. Details: " + ", ".join(error_messages)) + else: + return v + + @classmethod + def from_dict(cls, obj: Union[str, Dict[str, Any]]) -> Self: + return cls.from_json(json.dumps(obj)) @classmethod def from_json(cls, json_str: str) -> Self: - """Create an instance of OneOfEnumString from a JSON string""" - return cls(json.loads(json_str)) + """Returns the object represented by the json string""" + instance = cls.model_construct() + error_messages = [] + match = 0 + + # deserialize data into EnumString1 + try: + instance.actual_instance = EnumString1.from_json(json_str) + match += 1 + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + # deserialize data into EnumString2 + try: + instance.actual_instance = EnumString2.from_json(json_str) + match += 1 + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + + if match > 1: + # more than 1 match + raise ValueError("Multiple matches found when deserializing the JSON string into OneOfEnumString with oneOf schemas: EnumString1, EnumString2. Details: " + ", ".join(error_messages)) + elif match == 0: + # no match + raise ValueError("No match found when deserializing the JSON string into OneOfEnumString with oneOf schemas: EnumString1, EnumString2. Details: " + ", ".join(error_messages)) + else: + return instance + + def to_json(self) -> str: + """Returns the JSON representation of the actual instance""" + if self.actual_instance is None: + return "null" + + if hasattr(self.actual_instance, "to_json") and callable(self.actual_instance.to_json): + return self.actual_instance.to_json() + else: + return json.dumps(self.actual_instance) + + def to_dict(self) -> Optional[Union[Dict[str, Any], EnumString1, EnumString2]]: + """Returns the dict representation of the actual instance""" + if self.actual_instance is None: + return None + + if hasattr(self.actual_instance, "to_dict") and callable(self.actual_instance.to_dict): + return self.actual_instance.to_dict() + else: + # primitive type + return self.actual_instance + + def to_str(self) -> str: + """Returns the string representation of the actual instance""" + return pprint.pformat(self.model_dump()) diff --git a/samples/openapi3/client/petstore/python/petstore_api/models/with_nested_one_of.py b/samples/openapi3/client/petstore/python/petstore_api/models/with_nested_one_of.py index eaee08c294c6..eb7e90879e90 100644 --- a/samples/openapi3/client/petstore/python/petstore_api/models/with_nested_one_of.py +++ b/samples/openapi3/client/petstore/python/petstore_api/models/with_nested_one_of.py @@ -78,6 +78,9 @@ def to_dict(self) -> Dict[str, Any]: # override the default output from pydantic by calling `to_dict()` of nested_pig if self.nested_pig: _dict['nested_pig'] = self.nested_pig.to_dict() + # override the default output from pydantic by calling `to_dict()` of nested_oneof_enum_string + if self.nested_oneof_enum_string: + _dict['nested_oneof_enum_string'] = self.nested_oneof_enum_string.to_dict() # puts key-value pairs in additional_properties in the top level if self.additional_properties is not None: for _key, _value in self.additional_properties.items(): @@ -97,7 +100,7 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: _obj = cls.model_validate({ "size": obj.get("size"), "nested_pig": Pig.from_dict(obj["nested_pig"]) if obj.get("nested_pig") is not None else None, - "nested_oneof_enum_string": obj.get("nested_oneof_enum_string") + "nested_oneof_enum_string": OneOfEnumString.from_dict(obj["nested_oneof_enum_string"]) if obj.get("nested_oneof_enum_string") is not None else None }) # store additional fields in additional_properties for _key in obj.keys(): From 33cbaf274435b44f953b66ab2f6cb1466baad01b Mon Sep 17 00:00:00 2001 From: William Cheng Date: Sun, 7 Sep 2025 16:53:27 +0800 Subject: [PATCH 8/9] fix test file name --- .../java/org/openapitools/codegen/OpenAPINormalizerTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java index c212dc9bb6d6..454ffb35307d 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java @@ -156,7 +156,7 @@ public void testOpenAPINormalizerSimplifyOneOfAnyOfStringAndEnumString() { @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"); + OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_0/simplifyOneOfWithEnums_test.yaml"); // Test with rule enabled (default) Map options = new HashMap<>(); From 90ee2af625645453c91d9076907ac0a75775d840 Mon Sep 17 00:00:00 2001 From: William Cheng Date: Sun, 7 Sep 2025 17:16:21 +0800 Subject: [PATCH 9/9] fix incorrect filename --- .../java/org/openapitools/codegen/OpenAPINormalizerTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java index 454ffb35307d..202e227ce635 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java @@ -196,7 +196,7 @@ public void testSimplifyOneOfAnyOfEnum() throws Exception { assertNull(combinedRefsEnum.getOneOf()); // Test with rule disabled - OpenAPI openAPI2 = TestUtils.parseSpec("src/test/resources/3_0/simplifyoneOfWithEnums_test.yaml"); + OpenAPI openAPI2 = TestUtils.parseSpec("src/test/resources/3_0/simplifyOneOfWithEnums_test.yaml"); Map options2 = new HashMap<>(); options2.put("SIMPLIFY_ONEOF_ANYOF_ENUM", "false"); OpenAPINormalizer normalizer2 = new OpenAPINormalizer(openAPI2, options2);