Description
When an enum implements a generic interface (e.g. PersistableEnum<String>) and has @JsonValue on its getValue() method, the generated OpenAPI 3.1 schema non-deterministically omits "type": "string" for the enum property. The same code produces different output across JVM invocations (e.g. Maven Surefire vs IntelliJ test runner).
Root Cause
The Java compiler generates a bridge method Object getValue() alongside the real String getValue() when a class implements a generic interface. Crucially, the @JsonValue annotation is copied to the bridge method by the compiler:
public java.lang.String getValue(); // has @JsonValue ✓
public java.lang.Object getValue(); // ACC_BRIDGE, ACC_SYNTHETIC — also has @JsonValue ✓
In ModelResolver._createSchemaForEnum(), the @JsonValue method is found via:
clazz.getDeclaredMethods()
.filter(m -> m.isAnnotationPresent(JsonValue.class))
.filter(m -> m.getAnnotation(JsonValue.class).value())
.findFirst()
Both methods pass the filters. getDeclaredMethods() returns methods in unspecified order per JVM spec. findFirst() picks whichever comes first:
- If
String getValue() wins → PrimitiveType.fromType(String.class) → PrimitiveType.STRING → schema with correct type
- If
Object getValue() (bridge) wins → PrimitiveType.fromType(Object.class) → null → falls through to default schema creation
Additionally, in ModelResolver.resolve(), findJsonValueType(beanDescription) (which uses Jackson's BeanDescription.findJsonValueAccessor()) may or may not detect the @JsonValue and return early — before _createSchemaForEnum() is ever called. This creates a second source of non-determinism in how the enum schema is constructed.
Reproducer
// Generic interface
public interface PersistableEnum<T> {
T getValue();
}
// Enum with @JsonValue on getValue()
public enum PaymentState implements PersistableEnum<String> {
CREATED("created"),
IN_PROGRESS("in_progress"),
CONFIRMED("confirmed");
private final String value;
PaymentState(String value) { this.value = value; }
@JsonValue
@Override
public String getValue() { return value; }
}
// DTO using the enum
public class PaymentStatus {
private PaymentState state;
// getters/setters
}
Expected output (every time):
{
"state": {
"type": "string",
"enum": ["created", "in_progress", "confirmed"]
}
}
Actual output (non-deterministic — sometimes):
{
"state": {
"enum": ["created", "in_progress", "confirmed"]
}
}
The "type": "string" is missing because the bridge method was picked first by getDeclaredMethods().
Suggested Fix
In _createSchemaForEnum(), filter out bridge/synthetic methods before searching for @JsonValue:
Arrays.stream(clazz.getDeclaredMethods())
.filter(m -> !m.isBridge()) // ← add this
.filter(m -> m.isAnnotationPresent(JsonValue.class))
.filter(m -> m.getAnnotation(JsonValue.class).value())
.findFirst()
Environment
- swagger-core: 2.2.41
- springdoc-openapi: 2.8.15
- Jackson: 2.21.2
- Java: 25 (OpenJDK)
- OpenAPI mode: 3.1 (
openapi31 = true)
Description
When an enum implements a generic interface (e.g.
PersistableEnum<String>) and has@JsonValueon itsgetValue()method, the generated OpenAPI 3.1 schema non-deterministically omits"type": "string"for the enum property. The same code produces different output across JVM invocations (e.g. Maven Surefire vs IntelliJ test runner).Root Cause
The Java compiler generates a bridge method
Object getValue()alongside the realString getValue()when a class implements a generic interface. Crucially, the@JsonValueannotation is copied to the bridge method by the compiler:In
ModelResolver._createSchemaForEnum(), the@JsonValuemethod is found via:Both methods pass the filters.
getDeclaredMethods()returns methods in unspecified order per JVM spec.findFirst()picks whichever comes first:String getValue()wins →PrimitiveType.fromType(String.class)→PrimitiveType.STRING→ schema with correct typeObject getValue()(bridge) wins →PrimitiveType.fromType(Object.class)→null→ falls through to default schema creationAdditionally, in
ModelResolver.resolve(),findJsonValueType(beanDescription)(which uses Jackson'sBeanDescription.findJsonValueAccessor()) may or may not detect the@JsonValueand return early — before_createSchemaForEnum()is ever called. This creates a second source of non-determinism in how the enum schema is constructed.Reproducer
Expected output (every time):
{ "state": { "type": "string", "enum": ["created", "in_progress", "confirmed"] } }Actual output (non-deterministic — sometimes):
{ "state": { "enum": ["created", "in_progress", "confirmed"] } }The
"type": "string"is missing because the bridge method was picked first bygetDeclaredMethods().Suggested Fix
In
_createSchemaForEnum(), filter out bridge/synthetic methods before searching for@JsonValue:Environment
openapi31 = true)