Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multiple Superclasses Are Not Mapped To Multiple allOf If Used In Different Services #2601

Closed
aykborstelmann opened this issue May 24, 2024 · 0 comments

Comments

@aykborstelmann
Copy link

aykborstelmann commented May 24, 2024

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 and springdoc-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);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant