Skip to content

Commit 497bfae

Browse files
committed
PolymorphicModelConverter only handles direct subtypes and misses indirect. Fixes #2603
1 parent 07e2c92 commit 497bfae

File tree

4 files changed

+263
-9
lines changed

4 files changed

+263
-9
lines changed

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

+33-9
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,11 @@
2525
package org.springdoc.core.converters;
2626

2727
import java.lang.reflect.Modifier;
28+
import java.util.ArrayList;
2829
import java.util.Collection;
2930
import java.util.Iterator;
3031
import java.util.List;
32+
import java.util.stream.Collectors;
3133

3234
import com.fasterxml.jackson.databind.JavaType;
3335
import io.swagger.v3.core.converter.AnnotatedType;
@@ -61,10 +63,12 @@ public PolymorphicModelConverter(ObjectMapperProvider springDocObjectMapper) {
6163

6264
private static Schema<?> getResolvedSchema(JavaType javaType, Schema<?> resolvedSchema) {
6365
if (resolvedSchema instanceof ObjectSchema && resolvedSchema.getProperties() != null) {
64-
if (resolvedSchema.getProperties().containsKey(javaType.getRawClass().getName()))
66+
if (resolvedSchema.getProperties().containsKey(javaType.getRawClass().getName())){
6567
resolvedSchema = resolvedSchema.getProperties().get(javaType.getRawClass().getName());
66-
else if (resolvedSchema.getProperties().containsKey(javaType.getRawClass().getSimpleName()))
68+
}
69+
else if (resolvedSchema.getProperties().containsKey(javaType.getRawClass().getSimpleName())){
6770
resolvedSchema = resolvedSchema.getProperties().get(javaType.getRawClass().getSimpleName());
71+
}
6872
}
6973
return resolvedSchema;
7074
}
@@ -94,13 +98,8 @@ public Schema resolve(AnnotatedType type, ModelConverterContext context, Iterato
9498
*/
9599
private Schema composePolymorphicSchema(AnnotatedType type, Schema schema, Collection<Schema> schemas) {
96100
String ref = schema.get$ref();
97-
List<Schema> composedSchemas = schemas.stream()
98-
.filter(ComposedSchema.class::isInstance)
99-
.map(ComposedSchema.class::cast)
100-
.filter(s -> s.getAllOf() != null)
101-
.filter(s -> s.getAllOf().stream().anyMatch(s2 -> ref.equals(s2.get$ref())))
102-
.map(s -> new Schema().$ref(AnnotationsUtils.COMPONENTS_REF + s.getName()))
103-
.toList();
101+
List<Schema> composedSchemas = findComposedSchemas(ref, schemas);
102+
104103
if (composedSchemas.isEmpty()) return schema;
105104

106105
ComposedSchema result = new ComposedSchema();
@@ -109,6 +108,31 @@ private Schema composePolymorphicSchema(AnnotatedType type, Schema schema, Colle
109108
return result;
110109
}
111110

111+
/**
112+
* Find composed schemas recursively.
113+
*
114+
* @param ref the reference of the schema
115+
* @param schemas the collection of schemas to search in
116+
* @return the list of composed schemas
117+
*/
118+
private List<Schema> findComposedSchemas(String ref, Collection<Schema> schemas) {
119+
List<Schema> composedSchemas = schemas.stream()
120+
.filter(ComposedSchema.class::isInstance)
121+
.map(ComposedSchema.class::cast)
122+
.filter(s -> s.getAllOf() != null)
123+
.filter(s -> s.getAllOf().stream().anyMatch(s2 -> ref.equals(s2.get$ref())))
124+
.map(s -> new Schema().$ref(AnnotationsUtils.COMPONENTS_REF + s.getName()))
125+
.collect(Collectors.toList());
126+
127+
List<Schema> resultSchemas = new ArrayList<>(composedSchemas);
128+
129+
for (Schema childSchema : composedSchemas) {
130+
String childSchemaRef = childSchema.get$ref();
131+
resultSchemas.addAll(findComposedSchemas(childSchemaRef, schemas));
132+
}
133+
134+
return resultSchemas;
135+
}
112136
/**
113137
* Is concrete class boolean.
114138
*
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package test.org.springdoc.api.v30.app220;
2+
3+
4+
import com.fasterxml.jackson.annotation.JsonSubTypes;
5+
import com.fasterxml.jackson.annotation.JsonSubTypes.Type;
6+
import com.fasterxml.jackson.annotation.JsonTypeInfo;
7+
import io.swagger.v3.oas.annotations.media.Schema;
8+
9+
import org.springframework.web.bind.annotation.PostMapping;
10+
import org.springframework.web.bind.annotation.RequestBody;
11+
import org.springframework.web.bind.annotation.RestController;
12+
13+
@RestController
14+
public class HelloController {
15+
16+
@PostMapping("/parent")
17+
public void parentEndpoint(@RequestBody Superclass parent) {
18+
19+
}
20+
21+
}
22+
23+
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "@type")
24+
@JsonSubTypes({
25+
@Type(value = IntermediateClass.class, name = IntermediateClass.SCHEMA_NAME),
26+
})
27+
sealed class Superclass permits IntermediateClass {
28+
29+
public Superclass() {}
30+
}
31+
32+
@Schema(name = IntermediateClass.SCHEMA_NAME)
33+
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "@type")
34+
@JsonSubTypes({
35+
@Type(value = FirstChildClass.class, name = FirstChildClass.SCHEMA_NAME),
36+
@Type(value = SecondChildClass.class, name = SecondChildClass.SCHEMA_NAME)
37+
})
38+
sealed class IntermediateClass extends Superclass permits FirstChildClass, SecondChildClass {
39+
40+
public static final String SCHEMA_NAME = "IntermediateClass";
41+
}
42+
43+
@Schema(name = FirstChildClass.SCHEMA_NAME)
44+
final class FirstChildClass extends IntermediateClass {
45+
46+
public static final String SCHEMA_NAME = "Image";
47+
}
48+
49+
@Schema(name = SecondChildClass.SCHEMA_NAME)
50+
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "@type")
51+
@JsonSubTypes({
52+
@Type(value = ThirdChildClass.class, name = ThirdChildClass.SCHEMA_NAME)
53+
})
54+
sealed class SecondChildClass extends IntermediateClass {
55+
56+
public static final String SCHEMA_NAME = "Mail";
57+
}
58+
59+
@Schema(name = ThirdChildClass.SCHEMA_NAME)
60+
final class ThirdChildClass extends SecondChildClass {
61+
62+
public static final String SCHEMA_NAME = "Home";
63+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
*
3+
* *
4+
* * *
5+
* * * *
6+
* * * * * Copyright 2019-2022 the original author or authors.
7+
* * * * *
8+
* * * * * Licensed under the Apache License, Version 2.0 (the "License");
9+
* * * * * you may not use this file except in compliance with the License.
10+
* * * * * You may obtain a copy of the License at
11+
* * * * *
12+
* * * * * https://www.apache.org/licenses/LICENSE-2.0
13+
* * * * *
14+
* * * * * Unless required by applicable law or agreed to in writing, software
15+
* * * * * distributed under the License is distributed on an "AS IS" BASIS,
16+
* * * * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17+
* * * * * See the License for the specific language governing permissions and
18+
* * * * * limitations under the License.
19+
* * * *
20+
* * *
21+
* *
22+
*
23+
*/
24+
25+
package test.org.springdoc.api.v30.app220;
26+
27+
import test.org.springdoc.api.v30.AbstractSpringDocV30Test;
28+
29+
import org.springframework.boot.autoconfigure.SpringBootApplication;
30+
31+
public class SpringDocApp220Test extends AbstractSpringDocV30Test {
32+
33+
@SpringBootApplication
34+
static class SpringDocTestApp {}
35+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
{
2+
"openapi": "3.0.1",
3+
"info": {
4+
"title": "OpenAPI definition",
5+
"version": "v0"
6+
},
7+
"servers": [
8+
{
9+
"url": "http://localhost",
10+
"description": "Generated server url"
11+
}
12+
],
13+
"paths": {
14+
"/parent": {
15+
"post": {
16+
"tags": [
17+
"hello-controller"
18+
],
19+
"operationId": "parentEndpoint",
20+
"requestBody": {
21+
"content": {
22+
"application/json": {
23+
"schema": {
24+
"oneOf": [
25+
{
26+
"$ref": "#/components/schemas/Superclass"
27+
},
28+
{
29+
"$ref": "#/components/schemas/IntermediateClass"
30+
},
31+
{
32+
"$ref": "#/components/schemas/Image"
33+
},
34+
{
35+
"$ref": "#/components/schemas/Mail"
36+
},
37+
{
38+
"$ref": "#/components/schemas/Home"
39+
}
40+
]
41+
}
42+
}
43+
},
44+
"required": true
45+
},
46+
"responses": {
47+
"200": {
48+
"description": "OK"
49+
}
50+
}
51+
}
52+
}
53+
},
54+
"components": {
55+
"schemas": {
56+
"Home": {
57+
"type": "object",
58+
"allOf": [
59+
{
60+
"$ref": "#/components/schemas/Mail"
61+
}
62+
]
63+
},
64+
"Image": {
65+
"type": "object",
66+
"allOf": [
67+
{
68+
"$ref": "#/components/schemas/IntermediateClass"
69+
}
70+
]
71+
},
72+
"IntermediateClass": {
73+
"required": [
74+
"@type"
75+
],
76+
"type": "object",
77+
"discriminator": {
78+
"propertyName": "@type"
79+
},
80+
"allOf": [
81+
{
82+
"$ref": "#/components/schemas/Superclass"
83+
},
84+
{
85+
"type": "object",
86+
"properties": {
87+
"@type": {
88+
"type": "string"
89+
}
90+
}
91+
}
92+
]
93+
},
94+
"Mail": {
95+
"required": [
96+
"@type"
97+
],
98+
"type": "object",
99+
"discriminator": {
100+
"propertyName": "@type"
101+
},
102+
"allOf": [
103+
{
104+
"$ref": "#/components/schemas/IntermediateClass"
105+
},
106+
{
107+
"type": "object",
108+
"properties": {
109+
"@type": {
110+
"type": "string"
111+
}
112+
}
113+
}
114+
]
115+
},
116+
"Superclass": {
117+
"required": [
118+
"@type"
119+
],
120+
"type": "object",
121+
"properties": {
122+
"@type": {
123+
"type": "string"
124+
}
125+
},
126+
"discriminator": {
127+
"propertyName": "@type"
128+
}
129+
}
130+
}
131+
}
132+
}

0 commit comments

Comments
 (0)