Skip to content

Commit bd225bc

Browse files
fix: restore oneOf normalization behavior of dropping parent description for single oneOf alternative
1 parent 4043efc commit bd225bc

4 files changed

Lines changed: 161 additions & 15 deletions

File tree

‎modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java‎

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2681,7 +2681,9 @@ private static void restoreNullableInList(List<Schema> original, List<Schema> cl
26812681
* @return The simplified schema
26822682
*/
26832683
public static Schema simplifyOneOfAnyOfWithOnlyOneNonNullSubSchema(OpenAPI openAPI, Schema schema, List<Schema> subSchemas) {
2684-
if (subSchemas.removeIf(subSchema -> isNullTypeSchema(openAPI, subSchema))) {
2684+
boolean openapi31 = openAPI != null && SpecVersion.V31.equals(openAPI.getSpecVersion());
2685+
boolean nullSubSchemaRemoved = subSchemas.removeIf(subSchema -> isNullTypeSchema(openAPI, subSchema));
2686+
if (nullSubSchemaRemoved) {
26852687
schema.setNullable(true);
26862688
}
26872689

@@ -2691,11 +2693,17 @@ public static Schema simplifyOneOfAnyOfWithOnlyOneNonNullSubSchema(OpenAPI openA
26912693
// The parser may reuse a $ref schema instance in multiple locations. Clone the
26922694
// remaining $ref before applying parent metadata so those locations stay isolated.
26932695
if (subSchema.get$ref() != null) {
2694-
subSchema = cloneSchema(subSchema,
2695-
openAPI != null && SpecVersion.V31.equals(openAPI.getSpecVersion()));
2696-
}
2697-
// Preserve parent-level docs when nullable anyOf/oneOf collapses to a single child schema.
2698-
if (subSchema.getDescription() == null && schema.getDescription() != null) {
2696+
subSchema = cloneSchema(subSchema, openapi31);
2697+
}
2698+
// Preserve parent-level docs when anyOf/oneOf collapses to a single child schema.
2699+
// Exception: a non-nullable collapse to a $ref is a pure alias. Adding the parent
2700+
// description there would turn the $ref into a schema with sibling metadata and
2701+
// prevent the alias from being removed, so the description is dropped in that case.
2702+
boolean transferDescriptionToChild = subSchema.getDescription() == null && schema.getDescription() != null;
2703+
boolean collapsesToRefAlias = subSchema.get$ref() != null
2704+
&& !nullSubSchemaRemoved
2705+
&& !Boolean.TRUE.equals(schema.getNullable());
2706+
if (transferDescriptionToChild && !collapsesToRefAlias) {
26992707
subSchema.setDescription(schema.getDescription());
27002708
}
27012709
if (Boolean.TRUE.equals(schema.getNullable())) { // retain nullable setting

‎modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9502,6 +9502,19 @@ void oneOf_issue_14769() throws IOException {
95029502
.extendsClass("VehicleDto");
95039503
}
95049504

9505+
@Test
9506+
void oneOf_single_alternative_with_description_is_removed_with_default_normalization() throws IOException {
9507+
Map<String, File> files = generateFromContract("src/test/resources/3_0/oneOf_single_alternative_normalization.yaml", SPRING_BOOT,
9508+
Map.of(GENERATE_MODEL_DOCS, false, GENERATE_APIS, false, INTERFACE_ONLY, true));
9509+
9510+
assertThat(files).doesNotContainKey("Shape.java");
9511+
assertThat(files).containsKeys("Circle.java");
9512+
JavaFileAssert.assertThat(files.get("Circle.java")).fileContains("class Circle", "kind", "radius")
9513+
.fileDoesNotContain("description = \"A shape to draw.\"");
9514+
assertThat(files).containsKeys("NullableShape.java");
9515+
JavaFileAssert.assertThat(files.get("NullableShape.java")).fileContains("description = \"A shape to draw.\"");
9516+
}
9517+
95059518
@Test
95069519
void disableDiscriminatorJsonIgnorePropertiesIsTrueThenJsonIgnorePropertiesShouldBeNotAdded() throws IOException {
95079520
final Map<String, File> files = generateFromContract("src/test/resources/3_0/java/issue12777.yaml", SPRING_BOOT,

‎modules/openapi-generator/src/test/java/org/openapitools/codegen/utils/ModelUtilsTest.java‎

Lines changed: 93 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -700,32 +700,92 @@ public void simplifyAnyOfWithOnlyOneNonNullSubSchemaKeepsReadOnlyWriteOnlyAttrib
700700
public void simplifyOneOfAnyOfWithOnlyOneNonNullSubSchemaKeepsParentDescription() {
701701
OpenAPI openAPI = new OpenAPI();
702702

703-
Schema anyOfParent = new Schema().description("Access token");
704-
anyOfParent.setAnyOf(new ArrayList<>(Arrays.asList(
703+
Schema anyOfParent = anyOfSchema(
704+
"Access token",
705705
new StringSchema(),
706706
new Schema<>().type("null")
707-
)));
707+
);
708708
Schema anyOfSchema = ModelUtils.simplifyOneOfAnyOfWithOnlyOneNonNullSubSchema(openAPI, anyOfParent, anyOfParent.getAnyOf());
709709
assertEquals(anyOfSchema.getDescription(), "Access token");
710710

711-
Schema oneOfParent = new Schema().description("Expires at");
712-
oneOfParent.setOneOf(new ArrayList<>(Arrays.asList(
711+
Schema oneOfParent = oneOfSchema(
712+
"Expires at",
713713
new IntegerSchema(),
714714
new Schema<>().type("null")
715-
)));
715+
);
716716
Schema oneOfSchema = ModelUtils.simplifyOneOfAnyOfWithOnlyOneNonNullSubSchema(openAPI, oneOfParent, oneOfParent.getOneOf());
717717
assertEquals(oneOfSchema.getDescription(), "Expires at");
718718

719-
Schema anyOfParentWithChildDescription = new Schema().description("Parent description");
720-
anyOfParentWithChildDescription.setAnyOf(new ArrayList<>(Arrays.asList(
719+
Schema anyOfParentWithChildDescription = anyOfSchema(
720+
"Parent description",
721721
new StringSchema().description("Child description"),
722722
new Schema<>().type("null")
723-
)));
723+
);
724724
Schema anyOfSchemaWithChildDescription = ModelUtils.simplifyOneOfAnyOfWithOnlyOneNonNullSubSchema(
725725
openAPI,
726726
anyOfParentWithChildDescription,
727727
anyOfParentWithChildDescription.getAnyOf());
728728
assertEquals(anyOfSchemaWithChildDescription.getDescription(), "Child description");
729+
730+
Schema oneOfParentWithChildDescription = oneOfSchema(
731+
"Parent description",
732+
new StringSchema().description("Child description"),
733+
new Schema<>().type("null")
734+
);
735+
Schema oneOfSchemaWithChildDescription = ModelUtils.simplifyOneOfAnyOfWithOnlyOneNonNullSubSchema(
736+
openAPI,
737+
oneOfParentWithChildDescription,
738+
oneOfParentWithChildDescription.getOneOf());
739+
assertEquals(oneOfSchemaWithChildDescription.getDescription(), "Child description");
740+
}
741+
742+
@Test
743+
public void simplifySingleRefOneOfAnyOfDropsParentDescriptionToKeepAlias() {
744+
OpenAPI openAPI = new OpenAPI();
745+
746+
Schema refSchema = new Schema().$ref("#/components/schemas/IntegerRef");
747+
748+
Schema oneOfParent = oneOfSchema("Expires at", refSchema);
749+
Schema oneOfSchema = ModelUtils.simplifyOneOfAnyOfWithOnlyOneNonNullSubSchema(openAPI, oneOfParent, oneOfParent.getOneOf());
750+
751+
assertNull(oneOfSchema.getDescription());
752+
assertEquals(oneOfSchema.get$ref(), "#/components/schemas/IntegerRef");
753+
754+
Schema anyOfParent = anyOfSchema("Expires at", refSchema);
755+
Schema anyOfSchema = ModelUtils.simplifyOneOfAnyOfWithOnlyOneNonNullSubSchema(openAPI, anyOfParent, anyOfParent.getAnyOf());
756+
757+
assertNull(anyOfSchema.getDescription());
758+
assertEquals(anyOfSchema.get$ref(), "#/components/schemas/IntegerRef");
759+
760+
Schema nullableOneOfParent = oneOfSchema("Expires at", refSchema);
761+
nullableOneOfParent.setNullable(true);
762+
Schema nullableOneOfSchema = ModelUtils.simplifyOneOfAnyOfWithOnlyOneNonNullSubSchema(openAPI, nullableOneOfParent, nullableOneOfParent.getOneOf());
763+
764+
assertEquals(nullableOneOfSchema.getDescription(), "Expires at");
765+
assertEquals(nullableOneOfSchema.get$ref(), "#/components/schemas/IntegerRef");
766+
767+
Schema nullableAnyOfParent = anyOfSchema("Expires at", refSchema);
768+
nullableAnyOfParent.setNullable(true);
769+
Schema nullableAnyOfSchema = ModelUtils.simplifyOneOfAnyOfWithOnlyOneNonNullSubSchema(openAPI, nullableAnyOfParent, nullableAnyOfParent.getAnyOf());
770+
771+
assertEquals(nullableAnyOfSchema.getDescription(), "Expires at");
772+
assertEquals(nullableAnyOfSchema.get$ref(), "#/components/schemas/IntegerRef");
773+
}
774+
775+
@Test
776+
public void simplifySingleInlineOneOfAnyOfKeepsParentDescription() {
777+
OpenAPI openAPI = new OpenAPI();
778+
779+
Schema oneOfParent = oneOfSchema("Expires at");
780+
Schema oneOfSchema = ModelUtils.simplifyOneOfAnyOfWithOnlyOneNonNullSubSchema(openAPI, oneOfParent, oneOfParent.getOneOf());
781+
782+
assertEquals(oneOfSchema.getDescription(), "Expires at");
783+
assertNull(oneOfSchema.get$ref());
784+
785+
Schema anyOfParent = anyOfSchema("Expires at");
786+
Schema anyOfSchema = ModelUtils.simplifyOneOfAnyOfWithOnlyOneNonNullSubSchema(openAPI, anyOfParent, anyOfParent.getAnyOf());
787+
788+
assertEquals(anyOfSchema.getDescription(), "Expires at");
729789
}
730790

731791
@Test
@@ -1427,4 +1487,28 @@ private static Schema<?> nullableStringSchema() {
14271487
return schema;
14281488
}
14291489

1490+
private static Schema<?> oneOfSchema(String description) {
1491+
return oneOfSchema(description, new IntegerSchema());
1492+
}
1493+
1494+
private static Schema<?> oneOfSchema(String description, Schema<?>... alternatives) {
1495+
Schema<?> oneOfSchema = new Schema<>().description(description);
1496+
oneOfSchema.setOneOf(new ArrayList<>(Arrays.asList(
1497+
alternatives
1498+
)));
1499+
return oneOfSchema;
1500+
}
1501+
1502+
private static Schema<?> anyOfSchema(String description) {
1503+
return anyOfSchema(description, new IntegerSchema());
1504+
}
1505+
1506+
private static Schema<?> anyOfSchema(String description, Schema<?>... alternatives) {
1507+
Schema<?> anyOfSchema = new Schema<>().description(description);
1508+
anyOfSchema.setAnyOf(new ArrayList<>(Arrays.asList(
1509+
alternatives
1510+
)));
1511+
return anyOfSchema;
1512+
}
1513+
14301514
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
openapi: 3.0.0
2+
info:
3+
title: Shape API
4+
version: "1"
5+
paths:
6+
/shapes:
7+
post:
8+
operationId: createShape
9+
requestBody:
10+
content:
11+
application/json:
12+
schema:
13+
$ref: "#/components/schemas/Shape"
14+
responses:
15+
"201":
16+
description: Created
17+
components:
18+
schemas:
19+
# The default normalization behavior for a oneOf schema with a single alternative is to collapse the oneOf and use the child schema directly.
20+
# This is because the oneOf can be considered redundant when there is only one option.
21+
# The description is ignored since we want to ensure that the collapsed schema is treated as an alias and removed.
22+
Shape:
23+
description: A shape to draw.
24+
oneOf:
25+
- $ref: "#/components/schemas/Circle"
26+
NullableShape:
27+
description: A shape to draw.
28+
oneOf:
29+
- $ref: "#/components/schemas/Circle"
30+
- type: "null"
31+
Circle:
32+
type: object
33+
required:
34+
- kind
35+
- radius
36+
properties:
37+
kind:
38+
type: string
39+
radius:
40+
type: number
41+

0 commit comments

Comments
 (0)