swagger-core icon indicating copy to clipboard operation
swagger-core copied to clipboard

ModelResolver does not handle generics properly if class is annotated with @Schema(name="...")

Open daniel-frak opened this issue 2 years ago • 1 comments

When a generic class is annotated with @Schema having a non-null name, the TypeNameResolver does not get called, resulting in every instance of this class having the same name (as defined in @Schema), which breaks the OpenAPI description (although does not make it invalid in a technical sense).

The bug seems to be in this piece of code in ModelResolver::resolve:

String name = annotatedType.getName();
        if (StringUtils.isBlank(name)) {
            // allow override of name from annotation
            if (!annotatedType.isSkipSchemaName() && resolvedSchemaAnnotation != null && !resolvedSchemaAnnotation.name().isEmpty()) {
                name = resolvedSchemaAnnotation.name();
            }
            if (StringUtils.isBlank(name) && (type.isEnumType() || !ReflectionUtils.isSystemType(type))) {
                name = _typeName(type, beanDesc);
            }
        }

name = _typeName(type, beanDesc); gets called if there is no @Schema(name="...") annotation on the class, but the call gets omitted otherwise (because name is already defined via @Schema).

I've written a test to reproduce this issue:

import com.fasterxml.jackson.databind.type.TypeFactory;
import io.swagger.v3.core.converter.AnnotatedType;
import io.swagger.v3.core.converter.ModelConverters;
import io.swagger.v3.core.converter.ResolvedSchema;
import io.swagger.v3.oas.annotations.media.Schema;
import org.junit.jupiter.api.Test;

import java.lang.reflect.Type;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;

class BrokenGenericsTest {

    @Test
    void doesNotHandleGenericClassesWithSchemaAnnotationHavingNameProperty() {
        ResolvedSchema schemaForTestRecordWithoutAnnotation = getResolvedSchemaFor(TestRecordWithoutAnnotation.class);
        ResolvedSchema schemaForTestRecordWithAnnotation = getResolvedSchemaFor(TestRecordWithAnnotation.class);

        assertThat(schemaForTestRecordWithoutAnnotation.schema.getName())
                .isEqualTo("TestRecordWithoutAnnotationString");

        assertThat(schemaForTestRecordWithAnnotation.schema.getName())
                .isEqualTo("MyRecord"); // Should be MyRecordString!
    }

    private static ResolvedSchema getResolvedSchemaFor(Class<?> clazz) {
        Type type = TypeFactory.defaultInstance().constructCollectionLikeType(
                clazz, String.class);
        AnnotatedType annotatedType = new AnnotatedType(type);
        return ModelConverters.getInstance().resolveAsResolvedSchema(annotatedType);
    }

    private record TestRecordWithoutAnnotation<T>(
            List<T> content
    ) {
    }

    @Schema(name = "MyRecord")
    private record TestRecordWithAnnotation<T>(
            List<T> content
    ) {
    }
}

daniel-frak avatar Jun 24 '23 15:06 daniel-frak

i also have the same problem. In my application, there are two classes com.a.Response<T> and com.b.Response<T>. in order to avoid swagger display conflicts, i added annotations @Schema(name="ResponseA") and @Schema(name="ResponseB") respectively, in the Controller, the parameters received by two interfaces are com.a.Response<Foo>, com.a.Response<Bar>, i expected swagger to generate two schemas, ResponseAFoo and ResponseABar, but in fact swagger generated the same schema: ResponseA for these two parameters, resulting in a display error. my current solution is to remove @Schema(name="ResponseA"), @Schema(name="ResponseB") annotations, and then set springdoc.use-fqn=true, but the generated name is too long. i hope to support generic classes to add @Schema earlier

yuniansheng avatar Jul 25 '23 02:07 yuniansheng