From ed0db197af4ecb53334a71ddd6ef948d47240f2d Mon Sep 17 00:00:00 2001 From: Xi Lu Date: Mon, 22 Dec 2025 02:04:23 +0000 Subject: [PATCH 1/2] Fix normalization to prevent unnecessary allOf wrapping for oneOf and anyOf --- .../codegen/OpenAPINormalizer.java | 39 +++++++++-- .../codegen/OpenAPINormalizerTest.java | 66 +++++++++++++++++-- .../src/test/resources/3_1/anyOf.yaml | 34 ++++++++++ .../src/test/resources/3_1/oneOf.yaml | 42 ++++++++++++ 4 files changed, 169 insertions(+), 12 deletions(-) create mode 100644 modules/openapi-generator/src/test/resources/3_1/anyOf.yaml create mode 100644 modules/openapi-generator/src/test/resources/3_1/oneOf.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 b71ca5b96808..62312241fb9a 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 @@ -695,9 +695,21 @@ protected boolean isSelfReference(String name, Schema subSchema) { * @return Schema */ public Schema normalizeSchema(Schema schema, Set visitedSchemas) { + return normalizeSchema(schema, visitedSchemas, false); + } + + /** + * Normalizes a schema with optional composition context + * + * @param schema Schema + * @param visitedSchemas a set of visited schemas + * @param isInComposition true if schema is inside oneOf/anyOf composition + * @return Schema + */ + protected Schema normalizeSchema(Schema schema, Set visitedSchemas, boolean isInComposition) { // normalize reference schema if (StringUtils.isNotEmpty(schema.get$ref())) { - normalizeReferenceSchema(schema); + normalizeReferenceSchema(schema, isInComposition); } if (skipNormalization(schema, visitedSchemas)) { @@ -773,6 +785,21 @@ public Schema normalizeSchema(Schema schema, Set visitedSchemas) { * @param schema Schema */ protected void normalizeReferenceSchema(Schema schema) { + normalizeReferenceSchema(schema, false); + } + + /** + * Normalize reference schema with allOf to support sibling properties + * + * + * @param schema Schema + * @param isInComposition true if schema is inside oneOf/anyOf + */ + protected void normalizeReferenceSchema(Schema schema, boolean isInComposition) { + if (isInComposition) { + return; + } + if (schema.getTitle() != null || schema.getDescription() != null || schema.getNullable() != null || schema.getDefault() != null || schema.getDeprecated() != null || schema.getMaximum() != null || schema.getMinimum() != null @@ -860,7 +887,7 @@ protected void normalizeProperties(Map properties, Set v } for (Map.Entry propertiesEntry : properties.entrySet()) { Schema property = propertiesEntry.getValue(); - + // remove x-internal if needed (same logic as normalizeComponentsSchemas) if (property.getExtensions() != null && getRule(REMOVE_X_INTERNAL)) { Object xInternalValue = property.getExtensions().get(X_INTERNAL); @@ -1019,8 +1046,8 @@ protected Schema normalizeOneOf(Schema schema, Set visitedSchemas) { throw new RuntimeException("Error! oneOf schema is not of the type Schema: " + item); } - // update sub-schema with the updated schema - schema.getOneOf().set(i, normalizeSchema((Schema) item, visitedSchemas)); + // update sub-schema with the updated schema, passing isInComposition=true + schema.getOneOf().set(i, normalizeSchema((Schema) item, visitedSchemas, true)); } } else { // normalize it as it's no longer an oneOf @@ -1049,8 +1076,8 @@ protected Schema normalizeAnyOf(Schema schema, Set visitedSchemas) { throw new RuntimeException("Error! anyOf schema is not of the type Schema: " + item); } - // update sub-schema with the updated schema - schema.getAnyOf().set(i, normalizeSchema((Schema) item, visitedSchemas)); + // update sub-schema with the updated schema, passing isInComposition=true + schema.getAnyOf().set(i, normalizeSchema((Schema) item, visitedSchemas, true)); } // process rules here 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 eb85fed62eab..19aa7f0292b8 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 @@ -1266,36 +1266,90 @@ public void testRemoveXInternalFromInlineProperties() { OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_0/inline_x_internal_test.yaml"); Schema parentSchema = openAPI.getComponents().getSchemas().get("ParentSchema"); Schema inlineProperty = (Schema) parentSchema.getProperties().get("inlineXInternalProperty"); - + // Before normalization: x-internal should be present on inline property assertNotNull(inlineProperty.getExtensions()); assertEquals(inlineProperty.getExtensions().get("x-internal"), true); - + // Run normalizer with REMOVE_X_INTERNAL=true Map options = new HashMap<>(); options.put("REMOVE_X_INTERNAL", "true"); OpenAPINormalizer openAPINormalizer = new OpenAPINormalizer(openAPI, options); openAPINormalizer.normalize(); - + // After normalization: x-internal should be removed from inline property Schema parentSchemaAfter = openAPI.getComponents().getSchemas().get("ParentSchema"); Schema inlinePropertyAfter = (Schema) parentSchemaAfter.getProperties().get("inlineXInternalProperty"); - + // x-internal extension should be removed (null or not present in map) if (inlinePropertyAfter.getExtensions() != null) { assertNull(inlinePropertyAfter.getExtensions().get("x-internal")); } - + // The property itself should still exist (we're removing the flag, not the property) assertNotNull(inlinePropertyAfter); assertEquals(inlinePropertyAfter.getType(), "object"); - + // Nested properties should still exist assertNotNull(inlinePropertyAfter.getProperties()); assertNotNull(inlinePropertyAfter.getProperties().get("nestedField")); assertNotNull(inlinePropertyAfter.getProperties().get("nestedNumber")); } + /** + * Test oneOf items with only title (discriminator label) are NOT wrapped in allOf + */ + @Test + public void testOneOfWithOnlyTitleDoesNotNormalize() { + OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_1/oneOf.yaml"); + + Schema fruitSchema = openAPI.getComponents().getSchemas().get("fruit"); + assertNotNull(fruitSchema.getOneOf()); + assertEquals(fruitSchema.getOneOf().size(), 3); + + Schema item1Before = (Schema) fruitSchema.getOneOf().get(0); + assertEquals(item1Before.get$ref(), "#/components/schemas/apple"); + assertNull(item1Before.getAllOf()); + + OpenAPINormalizer normalizer = new OpenAPINormalizer(openAPI, new HashMap<>()); + normalizer.normalize(); + + Schema normalizedFruit = openAPI.getComponents().getSchemas().get("fruit"); + assertNotNull(normalizedFruit.getOneOf()); + assertEquals(normalizedFruit.getOneOf().size(), 3); + + Schema normalizedItem1 = (Schema) normalizedFruit.getOneOf().get(0); + assertEquals(normalizedItem1.get$ref(), "#/components/schemas/apple"); + assertNull(normalizedItem1.getAllOf()); + } + + /** + * Test anyOf items with only title (discriminator label) are NOT wrapped in allOf + */ + @Test + public void testAnyOfWithOnlyTitleDoesNotNormalize() { + OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_1/anyOf.yaml"); + + Schema fruitSchema = openAPI.getComponents().getSchemas().get("fruit"); + assertNotNull(fruitSchema.getAnyOf()); + assertEquals(fruitSchema.getAnyOf().size(), 2); + + Schema item1Before = (Schema) fruitSchema.getAnyOf().get(0); + assertEquals(item1Before.get$ref(), "#/components/schemas/apple"); + assertNull(item1Before.getAllOf()); + + OpenAPINormalizer normalizer = new OpenAPINormalizer(openAPI, new HashMap<>()); + normalizer.normalize(); + + Schema normalizedFruit = openAPI.getComponents().getSchemas().get("fruit"); + assertNotNull(normalizedFruit.getAnyOf()); + assertEquals(normalizedFruit.getAnyOf().size(), 2); + + Schema normalizedItem1 = (Schema) normalizedFruit.getAnyOf().get(0); + assertEquals(normalizedItem1.get$ref(), "#/components/schemas/apple"); + assertNull(normalizedItem1.getAllOf()); + } + public static class RemoveRequiredNormalizer extends OpenAPINormalizer { public RemoveRequiredNormalizer(OpenAPI openAPI, Map inputRules) { diff --git a/modules/openapi-generator/src/test/resources/3_1/anyOf.yaml b/modules/openapi-generator/src/test/resources/3_1/anyOf.yaml new file mode 100644 index 000000000000..5cd4e45931a4 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_1/anyOf.yaml @@ -0,0 +1,34 @@ +openapi: 3.1.0 +info: + title: fruity + version: 0.0.1 +paths: + /: + get: + responses: + '200': + description: desc + content: + application/json: + schema: + $ref: '#/components/schemas/fruit' +components: + schemas: + fruit: + title: fruit + type: object + anyOf: + - title: apple + $ref: '#/components/schemas/apple' + - title: banana + $ref: '#/components/schemas/banana' + apple: + type: object + properties: + kind: + type: string + banana: + type: object + properties: + count: + type: number diff --git a/modules/openapi-generator/src/test/resources/3_1/oneOf.yaml b/modules/openapi-generator/src/test/resources/3_1/oneOf.yaml new file mode 100644 index 000000000000..ddfccc0343b8 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_1/oneOf.yaml @@ -0,0 +1,42 @@ +openapi: 3.1.0 +info: + title: fruity + version: 0.0.1 +paths: + /: + get: + responses: + '200': + description: desc + content: + application/json: + schema: + $ref: '#/components/schemas/fruit' +components: + schemas: + fruit: + oneOf: + - title: appleChoice + $ref: '#/components/schemas/apple' + - title: bananaChoice + $ref: '#/components/schemas/banana' + - title: orangeChoice + $ref: '#/components/schemas/orange' + apple: + title: apple + type: object + properties: + kind: + type: string + banana: + title: banana + type: object + properties: + count: + type: number + orange: + title: orange + type: object + properties: + sweet: + type: boolean From dd1f9600c7a1a807d89190c05c1c5e0d3ebb6ef8 Mon Sep 17 00:00:00 2001 From: xil Date: Mon, 22 Dec 2025 19:38:26 +0000 Subject: [PATCH 2/2] remove unused normalizeReferenceSchema --- .../java/org/openapitools/codegen/OpenAPINormalizer.java | 9 --------- 1 file changed, 9 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 62312241fb9a..1bd05d5111bf 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 @@ -779,15 +779,6 @@ protected Schema normalizeSchema(Schema schema, Set visitedSchemas, bool return schema; } - /** - * Normalize reference schema with allOf to support sibling properties - * - * @param schema Schema - */ - protected void normalizeReferenceSchema(Schema schema) { - normalizeReferenceSchema(schema, false); - } - /** * Normalize reference schema with allOf to support sibling properties *