Skip to content

Commit ed0db19

Browse files
committed
Fix normalization to prevent unnecessary allOf wrapping for oneOf and anyOf
1 parent 23eff66 commit ed0db19

4 files changed

Lines changed: 169 additions & 12 deletions

File tree

modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -695,9 +695,21 @@ protected boolean isSelfReference(String name, Schema subSchema) {
695695
* @return Schema
696696
*/
697697
public Schema normalizeSchema(Schema schema, Set<Schema> visitedSchemas) {
698+
return normalizeSchema(schema, visitedSchemas, false);
699+
}
700+
701+
/**
702+
* Normalizes a schema with optional composition context
703+
*
704+
* @param schema Schema
705+
* @param visitedSchemas a set of visited schemas
706+
* @param isInComposition true if schema is inside oneOf/anyOf composition
707+
* @return Schema
708+
*/
709+
protected Schema normalizeSchema(Schema schema, Set<Schema> visitedSchemas, boolean isInComposition) {
698710
// normalize reference schema
699711
if (StringUtils.isNotEmpty(schema.get$ref())) {
700-
normalizeReferenceSchema(schema);
712+
normalizeReferenceSchema(schema, isInComposition);
701713
}
702714

703715
if (skipNormalization(schema, visitedSchemas)) {
@@ -773,6 +785,21 @@ public Schema normalizeSchema(Schema schema, Set<Schema> visitedSchemas) {
773785
* @param schema Schema
774786
*/
775787
protected void normalizeReferenceSchema(Schema schema) {
788+
normalizeReferenceSchema(schema, false);
789+
}
790+
791+
/**
792+
* Normalize reference schema with allOf to support sibling properties
793+
*
794+
*
795+
* @param schema Schema
796+
* @param isInComposition true if schema is inside oneOf/anyOf
797+
*/
798+
protected void normalizeReferenceSchema(Schema schema, boolean isInComposition) {
799+
if (isInComposition) {
800+
return;
801+
}
802+
776803
if (schema.getTitle() != null || schema.getDescription() != null
777804
|| schema.getNullable() != null || schema.getDefault() != null || schema.getDeprecated() != null
778805
|| schema.getMaximum() != null || schema.getMinimum() != null
@@ -860,7 +887,7 @@ protected void normalizeProperties(Map<String, Schema> properties, Set<Schema> v
860887
}
861888
for (Map.Entry<String, Schema> propertiesEntry : properties.entrySet()) {
862889
Schema property = propertiesEntry.getValue();
863-
890+
864891
// remove x-internal if needed (same logic as normalizeComponentsSchemas)
865892
if (property.getExtensions() != null && getRule(REMOVE_X_INTERNAL)) {
866893
Object xInternalValue = property.getExtensions().get(X_INTERNAL);
@@ -1019,8 +1046,8 @@ protected Schema normalizeOneOf(Schema schema, Set<Schema> visitedSchemas) {
10191046
throw new RuntimeException("Error! oneOf schema is not of the type Schema: " + item);
10201047
}
10211048

1022-
// update sub-schema with the updated schema
1023-
schema.getOneOf().set(i, normalizeSchema((Schema) item, visitedSchemas));
1049+
// update sub-schema with the updated schema, passing isInComposition=true
1050+
schema.getOneOf().set(i, normalizeSchema((Schema) item, visitedSchemas, true));
10241051
}
10251052
} else {
10261053
// normalize it as it's no longer an oneOf
@@ -1049,8 +1076,8 @@ protected Schema normalizeAnyOf(Schema schema, Set<Schema> visitedSchemas) {
10491076
throw new RuntimeException("Error! anyOf schema is not of the type Schema: " + item);
10501077
}
10511078

1052-
// update sub-schema with the updated schema
1053-
schema.getAnyOf().set(i, normalizeSchema((Schema) item, visitedSchemas));
1079+
// update sub-schema with the updated schema, passing isInComposition=true
1080+
schema.getAnyOf().set(i, normalizeSchema((Schema) item, visitedSchemas, true));
10541081
}
10551082

10561083
// process rules here

modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java

Lines changed: 60 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1266,36 +1266,90 @@ public void testRemoveXInternalFromInlineProperties() {
12661266
OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_0/inline_x_internal_test.yaml");
12671267
Schema parentSchema = openAPI.getComponents().getSchemas().get("ParentSchema");
12681268
Schema inlineProperty = (Schema) parentSchema.getProperties().get("inlineXInternalProperty");
1269-
1269+
12701270
// Before normalization: x-internal should be present on inline property
12711271
assertNotNull(inlineProperty.getExtensions());
12721272
assertEquals(inlineProperty.getExtensions().get("x-internal"), true);
1273-
1273+
12741274
// Run normalizer with REMOVE_X_INTERNAL=true
12751275
Map<String, String> options = new HashMap<>();
12761276
options.put("REMOVE_X_INTERNAL", "true");
12771277
OpenAPINormalizer openAPINormalizer = new OpenAPINormalizer(openAPI, options);
12781278
openAPINormalizer.normalize();
1279-
1279+
12801280
// After normalization: x-internal should be removed from inline property
12811281
Schema parentSchemaAfter = openAPI.getComponents().getSchemas().get("ParentSchema");
12821282
Schema inlinePropertyAfter = (Schema) parentSchemaAfter.getProperties().get("inlineXInternalProperty");
1283-
1283+
12841284
// x-internal extension should be removed (null or not present in map)
12851285
if (inlinePropertyAfter.getExtensions() != null) {
12861286
assertNull(inlinePropertyAfter.getExtensions().get("x-internal"));
12871287
}
1288-
1288+
12891289
// The property itself should still exist (we're removing the flag, not the property)
12901290
assertNotNull(inlinePropertyAfter);
12911291
assertEquals(inlinePropertyAfter.getType(), "object");
1292-
1292+
12931293
// Nested properties should still exist
12941294
assertNotNull(inlinePropertyAfter.getProperties());
12951295
assertNotNull(inlinePropertyAfter.getProperties().get("nestedField"));
12961296
assertNotNull(inlinePropertyAfter.getProperties().get("nestedNumber"));
12971297
}
12981298

1299+
/**
1300+
* Test oneOf items with only title (discriminator label) are NOT wrapped in allOf
1301+
*/
1302+
@Test
1303+
public void testOneOfWithOnlyTitleDoesNotNormalize() {
1304+
OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_1/oneOf.yaml");
1305+
1306+
Schema fruitSchema = openAPI.getComponents().getSchemas().get("fruit");
1307+
assertNotNull(fruitSchema.getOneOf());
1308+
assertEquals(fruitSchema.getOneOf().size(), 3);
1309+
1310+
Schema item1Before = (Schema) fruitSchema.getOneOf().get(0);
1311+
assertEquals(item1Before.get$ref(), "#/components/schemas/apple");
1312+
assertNull(item1Before.getAllOf());
1313+
1314+
OpenAPINormalizer normalizer = new OpenAPINormalizer(openAPI, new HashMap<>());
1315+
normalizer.normalize();
1316+
1317+
Schema normalizedFruit = openAPI.getComponents().getSchemas().get("fruit");
1318+
assertNotNull(normalizedFruit.getOneOf());
1319+
assertEquals(normalizedFruit.getOneOf().size(), 3);
1320+
1321+
Schema normalizedItem1 = (Schema) normalizedFruit.getOneOf().get(0);
1322+
assertEquals(normalizedItem1.get$ref(), "#/components/schemas/apple");
1323+
assertNull(normalizedItem1.getAllOf());
1324+
}
1325+
1326+
/**
1327+
* Test anyOf items with only title (discriminator label) are NOT wrapped in allOf
1328+
*/
1329+
@Test
1330+
public void testAnyOfWithOnlyTitleDoesNotNormalize() {
1331+
OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_1/anyOf.yaml");
1332+
1333+
Schema fruitSchema = openAPI.getComponents().getSchemas().get("fruit");
1334+
assertNotNull(fruitSchema.getAnyOf());
1335+
assertEquals(fruitSchema.getAnyOf().size(), 2);
1336+
1337+
Schema item1Before = (Schema) fruitSchema.getAnyOf().get(0);
1338+
assertEquals(item1Before.get$ref(), "#/components/schemas/apple");
1339+
assertNull(item1Before.getAllOf());
1340+
1341+
OpenAPINormalizer normalizer = new OpenAPINormalizer(openAPI, new HashMap<>());
1342+
normalizer.normalize();
1343+
1344+
Schema normalizedFruit = openAPI.getComponents().getSchemas().get("fruit");
1345+
assertNotNull(normalizedFruit.getAnyOf());
1346+
assertEquals(normalizedFruit.getAnyOf().size(), 2);
1347+
1348+
Schema normalizedItem1 = (Schema) normalizedFruit.getAnyOf().get(0);
1349+
assertEquals(normalizedItem1.get$ref(), "#/components/schemas/apple");
1350+
assertNull(normalizedItem1.getAllOf());
1351+
}
1352+
12991353
public static class RemoveRequiredNormalizer extends OpenAPINormalizer {
13001354

13011355
public RemoveRequiredNormalizer(OpenAPI openAPI, Map<String, String> inputRules) {
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
openapi: 3.1.0
2+
info:
3+
title: fruity
4+
version: 0.0.1
5+
paths:
6+
/:
7+
get:
8+
responses:
9+
'200':
10+
description: desc
11+
content:
12+
application/json:
13+
schema:
14+
$ref: '#/components/schemas/fruit'
15+
components:
16+
schemas:
17+
fruit:
18+
title: fruit
19+
type: object
20+
anyOf:
21+
- title: apple
22+
$ref: '#/components/schemas/apple'
23+
- title: banana
24+
$ref: '#/components/schemas/banana'
25+
apple:
26+
type: object
27+
properties:
28+
kind:
29+
type: string
30+
banana:
31+
type: object
32+
properties:
33+
count:
34+
type: number
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
openapi: 3.1.0
2+
info:
3+
title: fruity
4+
version: 0.0.1
5+
paths:
6+
/:
7+
get:
8+
responses:
9+
'200':
10+
description: desc
11+
content:
12+
application/json:
13+
schema:
14+
$ref: '#/components/schemas/fruit'
15+
components:
16+
schemas:
17+
fruit:
18+
oneOf:
19+
- title: appleChoice
20+
$ref: '#/components/schemas/apple'
21+
- title: bananaChoice
22+
$ref: '#/components/schemas/banana'
23+
- title: orangeChoice
24+
$ref: '#/components/schemas/orange'
25+
apple:
26+
title: apple
27+
type: object
28+
properties:
29+
kind:
30+
type: string
31+
banana:
32+
title: banana
33+
type: object
34+
properties:
35+
count:
36+
type: number
37+
orange:
38+
title: orange
39+
type: object
40+
properties:
41+
sweet:
42+
type: boolean

0 commit comments

Comments
 (0)