-
-
Notifications
You must be signed in to change notification settings - Fork 553
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-apiandspringdoc-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);