Skip to content

Commit a8e9f2b

Browse files
committed
fix: return composed oneOf polymorphic schema and remove duplicated _links
1 parent fe82ab5 commit a8e9f2b

6 files changed

Lines changed: 89 additions & 39 deletions

File tree

springdoc-openapi-starter-common/src/main/java/org/springdoc/core/converters/PolymorphicModelConverter.java

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,28 @@ else if (resolvedSchema.getProperties().containsKey(javaType.getRawClass().getSi
120120
return resolvedSchema;
121121
}
122122

123+
/**
124+
* Removes _links from allOf child schemas to prevent duplication.
125+
* In allOf composition, child schemas (allOf[1+]) should not redefine
126+
* inherited properties like _links that come from the parent (allOf[0]).
127+
*
128+
* @param composedSchema the composed schema with allOf structure
129+
*/
130+
private void removeLinksFromAllOfChild(ComposedSchema composedSchema) {
131+
List<Schema> allOf = composedSchema.getAllOf();
132+
if (allOf != null && allOf.size() > 1) {
133+
// allOf[0]는 부모 스키마 (allOf 첫 번째)
134+
// allOf[1+]는 자식의 고유 속성들 (allOf 두 번째부터)
135+
for (int i = 1; i < allOf.size(); i++) {
136+
Schema childSchema = allOf.get(i);
137+
if (childSchema != null && childSchema.getProperties() != null) {
138+
// _links 제거 (부모로부터 상속됨)
139+
childSchema.getProperties().remove("_links");
140+
}
141+
}
142+
}
143+
}
144+
123145
@Override
124146
public Schema resolve(AnnotatedType type, ModelConverterContext context, Iterator<ModelConverter> chain) {
125147
JavaType javaType = springDocObjectMapper.jsonMapper().constructType(type.getType());
@@ -147,7 +169,14 @@ public Schema resolve(AnnotatedType type, ModelConverterContext context, Iterato
147169
type.resolveAsRef(true);
148170
Schema<?> resolvedSchema = chain.next().resolve(type, context, chain);
149171
resolvedSchema = getResolvedSchema(javaType, resolvedSchema);
150-
if (resolvedSchema == null || resolvedSchema.get$ref() == null) {
172+
173+
if (resolvedSchema instanceof ComposedSchema composedSchema &&
174+
composedSchema.getAllOf() != null &&
175+
!composedSchema.getAllOf().isEmpty()) {
176+
removeLinksFromAllOfChild(composedSchema);
177+
}
178+
179+
if (resolvedSchema == null || resolvedSchema.get$ref() == null) {
151180
return resolvedSchema;
152181
}
153182
if (resolvedSchema.get$ref().contains(Components.COMPONENTS_SCHEMAS_REF)) {
@@ -175,24 +204,21 @@ private Schema composePolymorphicSchema(AnnotatedType type, Schema schema, Colle
175204
String ref = schema.get$ref();
176205
List<Schema> composedSchemas = findComposedSchemas(ref, schemas);
177206
if (composedSchemas.isEmpty()) return schema;
178-
ComposedSchema result = new ComposedSchema();
207+
ComposedSchema result = new ComposedSchema();
179208
if (isConcreteClass(type)) result.addOneOfItem(schema);
180209
JavaType javaType = springDocObjectMapper.jsonMapper().constructType(type.getType());
181210
Class<?> clazz = javaType.getRawClass();
182211
if (TYPES_TO_SKIP.stream().noneMatch(typeToSkip -> typeToSkip.equals(clazz.getSimpleName())))
183212
composedSchemas.forEach(result::addOneOfItem);
184213

185-
// Remove _links from child schemas to prevent duplication in allOf
186-
// The _links field is inherited from RepresentationModel and handled by HateoasLinksConverter
187-
boolean hasParentReference = schemas.stream()
188-
.anyMatch(s -> s.get$ref() != null);
189-
190-
if (hasParentReference && schema != null && schema.getProperties() != null) {
191-
schema.getProperties().remove("_links");
214+
// Remove _links from result (composed schema) to prevent duplication
215+
if (result.getOneOf() != null) {
216+
result.getOneOf().stream()
217+
.filter(s -> s.getProperties() != null)
218+
.forEach(s -> s.getProperties().remove("_links"));
192219
}
193220

194-
// ... rest of existing code ...
195-
return schema;
221+
return result;
196222
}
197223

198224
/**

springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/java/test/org/springdoc/api/v30/app11/HateoasController.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import org.springframework.web.bind.annotation.RestController;
77

88
@RestController
9-
@Tag(name = "Hateoas", description = "HATEOAS with allOf composition test")
9+
@Tag(name = "hateoas-controller", description = "Hateoas Controller")
1010
public class HateoasController {
1111

1212
@GetMapping(path = "/test-dto", produces = "application/json")

springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/java/test/org/springdoc/api/v31/app13/ExtendedTestDto.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@
88
* ensuring _links is not duplicated in the child schema.
99
*/
1010
@Schema(
11-
description = "Extended DTO with allOf composition",
12-
allOf = {TestDto.class}
11+
description = "Extended DTO with allOf composition"
1312
)
1413
public class ExtendedTestDto extends TestDto {
1514

springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/java/test/org/springdoc/api/v31/app13/HateoasController.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import org.springframework.web.bind.annotation.GetMapping;
77

88
@RestController
9-
@Tag(name = "Hateoas", description = "HATEOAS with allOf composition test")
9+
@Tag(name = "hateoas-controller", description = "Hateoas Controller")
1010
public class HateoasController {
1111

1212
@GetMapping(path = "/test-dto", produces = "application/json")

springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/resources/results/3.0.1/app11.json

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
12
{
23
"openapi": "3.0.1",
34
"info": {
@@ -12,14 +13,16 @@
1213
],
1314
"tags": [
1415
{
15-
"name": "Hateoas",
16-
"description": "HATEOAS with allOf composition test"
16+
"name": "hateoas-controller",
17+
"description": "Hateoas Controller"
1718
}
1819
],
1920
"paths": {
2021
"/test-dto": {
2122
"get": {
22-
"tags": ["Hateoas"],
23+
"tags": [
24+
"hateoas-controller"
25+
],
2326
"summary": "Get Test DTO",
2427
"description": "Returns a TestDto with HATEOAS links",
2528
"operationId": "getTestDto",
@@ -29,7 +32,14 @@
2932
"content": {
3033
"application/json": {
3134
"schema": {
32-
"$ref": "#/components/schemas/TestDto"
35+
"oneOf": [
36+
{
37+
"$ref": "#/components/schemas/TestDto"
38+
},
39+
{
40+
"$ref": "#/components/schemas/ExtendedTestDto"
41+
}
42+
]
3343
}
3444
}
3545
}
@@ -39,7 +49,9 @@
3949
},
4050
"/extended-test-dto": {
4151
"get": {
42-
"tags": ["Hateoas"],
52+
"tags": [
53+
"hateoas-controller"
54+
],
4355
"summary": "Get Extended Test DTO",
4456
"description": "Returns an ExtendedTestDto with HATEOAS links",
4557
"operationId": "getExtendedTestDto",
@@ -67,7 +79,10 @@
6779
"type": "string"
6880
},
6981
"_links": {
70-
"$ref": "#/components/schemas/Links"
82+
"type": "array",
83+
"items": {
84+
"$ref": "#/components/schemas/Link"
85+
}
7186
}
7287
},
7388
"description": "Parent DTO extending RepresentationModel"
@@ -89,12 +104,6 @@
89104
],
90105
"description": "Extended DTO with allOf composition"
91106
},
92-
"Links": {
93-
"type": "object",
94-
"additionalProperties": {
95-
"$ref": "#/components/schemas/Link"
96-
}
97-
},
98107
"Link": {
99108
"type": "object",
100109
"properties": {

springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/resources/results/3.1.0/app13.json

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,16 @@
1313
],
1414
"tags": [
1515
{
16-
"name": "Hateoas",
17-
"description": "HATEOAS with allOf composition test"
16+
"name": "hateoas-controller",
17+
"description": "Hateoas Controller"
1818
}
1919
],
2020
"paths": {
2121
"/test-dto": {
2222
"get": {
23-
"tags": ["Hateoas"],
23+
"tags": [
24+
"hateoas-controller"
25+
],
2426
"summary": "Get Test DTO",
2527
"description": "Returns a TestDto with HATEOAS links",
2628
"operationId": "getTestDto",
@@ -30,7 +32,14 @@
3032
"content": {
3133
"application/json": {
3234
"schema": {
33-
"$ref": "#/components/schemas/TestDto"
35+
"oneOf": [
36+
{
37+
"$ref": "#/components/schemas/TestDto"
38+
},
39+
{
40+
"$ref": "#/components/schemas/ExtendedTestDto"
41+
}
42+
]
3443
}
3544
}
3645
}
@@ -40,7 +49,9 @@
4049
},
4150
"/extended-test-dto": {
4251
"get": {
43-
"tags": ["Hateoas"],
52+
"tags": [
53+
"hateoas-controller"
54+
],
4455
"summary": "Get Extended Test DTO",
4556
"description": "Returns an ExtendedTestDto with HATEOAS links",
4657
"operationId": "getExtendedTestDto",
@@ -68,7 +79,10 @@
6879
"type": "string"
6980
},
7081
"_links": {
71-
"$ref": "#/components/schemas/Links"
82+
"type": "array",
83+
"items": {
84+
"$ref": "#/components/schemas/Link"
85+
}
7286
}
7387
},
7488
"description": "Parent DTO extending RepresentationModel"
@@ -77,16 +91,18 @@
7791
"allOf": [
7892
{
7993
"$ref": "#/components/schemas/TestDto"
94+
},
95+
{
96+
"type": "object",
97+
"properties": {
98+
"otherField": {
99+
"type": "string"
100+
}
101+
}
80102
}
81103
],
82104
"description": "Extended DTO with allOf composition"
83105
},
84-
"Links": {
85-
"type": "object",
86-
"additionalProperties": {
87-
"$ref": "#/components/schemas/Link"
88-
}
89-
},
90106
"Link": {
91107
"type": "object",
92108
"properties": {

0 commit comments

Comments
 (0)