Description
Describe the bug
Multiple superclasses of response types (i.e. via Interfaces) are not mapped correctly,
if the superclasses are used in different return types.
To Reproduce
Steps to reproduce the behavior:
Suppose the following dto hierarchy:
@JsonTypeInfo(use = Id.NAME, property = "@type")
@JsonSubTypes(@Type(CommonImplementor.class))
interface FirstHierarchy {}
@JsonTypeInfo(use = Id.NAME, property = "@type")
@JsonSubTypes(@Type(CommonImplementor.class))
interface SecondHierarchy {}
class CommonImplementor implements FirstHierarchy, SecondHierarchy {}
and the following return types for request methods.
record CommonImplementorUser(FirstHierarchy firstHierarchy, SecondHierarchy secondHierarchy) {}
record FirstHierarchyUser(FirstHierarchy firstHierarchy) {}
record SecondHierarchyUser(SecondHierarchy secondHierarchy) {}
If we use CommonImplementorUser
as return type for a request method, everything is mapped correctly, i.e.
CommonImplementor:
type: object
allOf:
- $ref: '#/components/schemas/FirstHierarchy'
- $ref: '#/components/schemas/SecondHierarchy'
However, if we use FirstHierarchyUser
as return type for one request method and SecondHierarchyUser
as return type of a second, we get only:
CommonImplementor:
type: object
allOf:
- $ref: '#/components/schemas/FirstHierarchy'
or
CommonImplementor:
type: object
allOf:
- $ref: '#/components/schemas/SecondHierarchy'
depending on the order of processing.
- What version of spring-boot you are using: 3.2.5
- What modules and versions of springdoc-openapi are you using: 2.5.0 [
springdoc-openapi-starter-common
,springdoc-openapi-starter-webmvc-api
andspringdoc-openapi-starter-webmvc-ui
]
Expected behavior
I would always expect the following to be generated:
CommonImplementor:
type: object
allOf:
- $ref: '#/components/schemas/FirstHierarchy'
- $ref: '#/components/schemas/SecondHierarchy'
Additional context
I already debugged the problem, s.t. I am quite sure that, the call of io.swagger.v3.core.converter.ModelConverters#resolveAsResolvedSchema
in org.springdoc.core.utils.SpringDocAnnotationsUtils#extractSchema
is the problem.
io.swagger.v3.core.converter.ModelConverters#resolveAsResolvedSchema
look the following:
public ResolvedSchema resolveAsResolvedSchema(AnnotatedType type) {
ModelConverterContextImpl context = new ModelConverterContextImpl(
converters);
ResolvedSchema resolvedSchema = new ResolvedSchema();
resolvedSchema.schema = context.resolve(type);
resolvedSchema.referencedSchemas = context.getDefinedModels();
return resolvedSchema;
}
Thus for every processed return type, a new ModelConverterContextImpl
is created and used to go through the tree, if simply one context would be used for all annotated types, this would be no problem, since swagger is smart enough to extend existing types.
Therefore in one case CommonImplementor
is reached from FirstHierarchy
and in the other from SecondHierarchy
.
However, org.springdoc.core.utils.SpringDocAnnotationsUtils#extractSchema
does not actually merge the schemas, but rather "decides" for one:
Map<String, Schema> componentSchemas = components.getSchemas();
if (componentSchemas == null) {
componentSchemas = new LinkedHashMap<>();
componentSchemas.putAll(schemaMap);
}
else
for (Map.Entry<String, Schema> entry : schemaMap.entrySet()) {
// If we've seen this schema before but find later it should be polymorphic,
// replace the existing schema with this richer version.
if (!componentSchemas.containsKey(entry.getKey()) ||
(!entry.getValue().getClass().equals(componentSchemas.get(entry.getKey()).getClass()) && entry.getValue().getAllOf() != null)) {
componentSchemas.put(entry.getKey(), entry.getValue());
}
}
components.setSchemas(componentSchemas);