ModelResolver does not handle generics properly if class is annotated with @Schema(name="...")
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
) {
}
}
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