swagger-api / swagger-api/swagger-core
ModelResolver does not handle generics properly if class is annotated with @Schema(name="...")
Nobody has claimed this yet.
- Dominant language
- Java
- Stars
- 7.5k
- Forks
- 2.3k
- Avg merge
- 18h 1m
- Merged PRs (30d)
- 10
Description
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
) {
}
}
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in ModelResolver.resolve, focusing on the name-selection logic shown in the issue and how TypeNameResolver is reached for generic types. Use the BrokenGenericsTest reproduction with TestRecordWithAnnotation and TestRecordWithoutAnnotation; done means the annotated generic resolves to "MyRecordString" and the regression test passes.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- java, openapi
- Domain
- backend-api-design
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Stale
- Clarity
- Clearly specified
- Newbie friendliness
- 35/100