Skip to content

How to prevent oneOf() on supertypes leading to recursion in subtypes? #3677

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

Open
ckaag opened this issue Aug 9, 2020 · 2 comments
Open

Comments

@ckaag
Copy link

ckaag commented Aug 9, 2020

While debugging an issue on Javalin's OpenAPI plugin ( javalin/javalin#1032 ) I found following behavior inside swagger-core at the following line:

https://github.com/swagger-api/swagger-core/blob/v2.1.2/modules/swagger-core/src/main/java/io/swagger/v3/core/jackson/ModelResolver.java#L1306

My scenario is as follows:

  • Assume you have supertype S of both subtypes A and B.
  • You use the annotation-package's @Schema(oneOf = {A.class, B.class}) on S.

This will end up with S getting the oneOf-Attribute as requested BUT both A and B thanks to this line get allOf = {S.class} assigned by the ModelResolver. As far as I understand this is to deal with inheritance.

The result of this from an API perspective: if S is specified, the value may be A or B. But A is allOf(S), so transitively it's also oneOf(A, B). While in this isolated example, this is still fine, because the specific types are transparent if they're only ever used as S. But assume A is also reused somewhere else in the API specification: in this case the resulting Schema object would allow you to replace A with allOf(A, B) which seems to be not precise (at least in languages that aren't dynamically- or duck-typed).

Where did my thinking in this use case go wrong?

Or to ask the question on a higher abstraction: How can I structure my input to the ModelResolver using oneOf designation on the superclass without this recursive specification result on the subclass?

Thanks for any input on this!

@Yacubane
Copy link

@ckaag Have you found any solution?

@Yacubane
Copy link

I have managed to do it with some util that flattens and removes allOf schemas.

Util code:

package ...;

import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.media.Schema;

import java.util.HashMap;
import java.util.List;

public class AllOfSchemaFlattener {

    @SuppressWarnings({"rawtypes", "unchecked"})
    public static void flattenAllOf(OpenAPI openApi) {
        var schemas = openApi.getComponents().getSchemas();
        schemas.forEach((name, schema) -> {
            if (hasAllOf(schema)) {
                var allProperties = new HashMap<String, Schema>();

                // Put all properties of the current schema
                if (schema.getProperties() != null) allProperties.putAll(schema.getProperties());

                var allOfSchemas = (List<Schema<?>>) schema.getAllOf();
                allOfSchemas.forEach(parentSchema -> {
                    if (hasAllOf(parentSchema)) throw new UnsupportedAllOfFlatteningException();
                    if (parentSchema.get$ref() != null) {
                        // Resolve $ref to fetch the actual parent schema and get its properties
                        String refName = parentSchema.get$ref().replace("#/components/schemas/", "");
                        Schema<?> referencedSchema = schemas.get(refName);
                        if (hasAllOf(referencedSchema)) throw new UnsupportedAllOfFlatteningException();
                        if (referencedSchema.getProperties() != null) {
                            allProperties.putAll(referencedSchema.getProperties());
                        }
                    } else if (parentSchema.getProperties() != null) {
                        // Put all properties of the parent schema
                        allProperties.putAll(parentSchema.getProperties());
                    }
                });

                schema.setProperties(allProperties); // Set all properties (with parent properties)
                schema.setAllOf(null); // Remove allOf after flattening properties
            }
        });
    }

    private static boolean hasAllOf(Schema<?> schema) {
        return schema.getAllOf() != null && !schema.getAllOf().isEmpty();
    }

    private static class UnsupportedAllOfFlatteningException extends RuntimeException {
        public UnsupportedAllOfFlatteningException() {
            super("Nested allOf flattening is not supported, consider rewriting AllOfSchemaFlattener class code to flatten recursively");
        }
    }
}

And then use it:
.addOpenApiCustomizer(AllOfSchemaFlattener::flattenAllOf)

If your schema contains nested allOf schemas, you will need to rewrite that code to recursively go through allOf schemas and get all properties, it should not be hard. I have added custom exception for that code to be able to test my API against such case and for my case it's not needed. I have tested it on quite big API and it works. Also I had to add springdoc.model-converters.polymorphic-converter.enabled=false to configuration to disable automatically creating oneOf and using ref instead.

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

2 participants