FasterXML/jackson-databind · error · IllegalStateException
AnnotationIntrospector returned `Class<${deserClass.getName(
Error message
AnnotationIntrospector returned `Class<${deserClass.getName()}>`; expected `Class<ValueDeserializer>` What it means
Thrown when a custom AnnotationIntrospector (or @JsonDeserialize.using/as) returns a Class object as the deserializer definition, but that Class does not implement ValueDeserializer. Jackson validates the type before instantiating it via HandlerInstantiator or reflection, so this guards against a wrong class being wired in as a deserializer handler.
Source
Thrown at src/main/java/tools/jackson/databind/deser/DeserializationContextExt.java:256
ValueDeserializer<?> deser;
if (deserDef instanceof ValueDeserializer valueDeserializer) {
deser = valueDeserializer;
} else {
// Alas, there's no way to force return type of "either class
// X or Y" -- need to throw an exception after the fact
if (!(deserDef instanceof Class)) {
throw new IllegalStateException("AnnotationIntrospector returned deserializer definition of type "
+deserDef.getClass().getName()
+"; expected type `ValueDeserializer` or `Class<ValueDeserializer>` instead");
}
Class<?> deserClass = (Class<?>)deserDef;
// there are some known "no class" markers to consider too:
if (deserClass == ValueDeserializer.None.class || ClassUtil.isBogusClass(deserClass)) {
return null;
}
if (!ValueDeserializer.class.isAssignableFrom(deserClass)) {
throw new IllegalStateException("AnnotationIntrospector returned `Class<"+deserClass.getName()+">`; expected `Class<ValueDeserializer>`");
}
HandlerInstantiator hi = _config.getHandlerInstantiator();
deser = (hi == null) ? null : hi.deserializerInstance(_config, ann, deserClass);
if (deser == null) {
deser = (ValueDeserializer<?>) ClassUtil.createInstance(deserClass,
_config.canOverrideAccessModifiers());
}
}
// First: need to resolve
deser.resolve(this);
return (ValueDeserializer<Object>) deser;
}
@Override
public final KeyDeserializer keyDeserializerInstance(Annotated ann, Object deserDef)
{
if (deserDef == null) {
return null;View on GitHub (pinned to 87876ca5c0)
Solutions
- Verify the class referenced by @JsonDeserialize(as = ...) or returned by your AnnotationIntrospector actually implements tools.jackson.databind.ValueDeserializer.
- If you meant to customize serialization, move the annotation to @JsonSerialize(using = ...)/@JsonSerialize(as = ...).
- Return an instance (ValueDeserializer) instead of a Class, or return ValueDeserializer.None.class to signal 'no deserializer'.
- If using a HandlerInstantiator, ensure it returns a ValueDeserializer instance and that the registered class implements ValueDeserializer.
Example fix
// before @JsonDeserialize(as = MySerializer.class) // wrong: serializer, not deserializer private Foo foo; // after @JsonDeserialize(using = MyFooDeserializer.class) // must extend ValueDeserializer<Foo> private Foo foo;
Defensive patterns
Strategy: validation
Validate before calling
Class<?> c = (Class<?>) def;
if (c != null && c != ValueDeserializer.None.class
&& !ValueDeserializer.class.isAssignableFrom(c)) {
throw new IllegalStateException("Not a ValueDeserializer: " + c);
} Type guard
static boolean isValidDeserializerDef(Object def) {
return def == null
|| def instanceof ValueDeserializer
|| (def instanceof Class<?> c
&& (c == ValueDeserializer.None.class
|| ValueDeserializer.class.isAssignableFrom(c)));
} Try / catch
try { mapper.readValue(json, Foo.class); }
catch (IllegalStateException e) {
if (e.getMessage().contains("expected `Class<ValueDeserializer>`")) {
// fix the @JsonDeserialize(as=...) wiring
} else throw e;
} Prevention
- Always annotate deserializers with @JsonDeserialize(using=...) and serializers with @JsonSerialize(...); never swap as/using blindly.
- Unit-test custom AnnotationIntrospector overrides to assert the returned type for each find* method.
When it happens
Trigger: Registering @JsonDeserialize(as = SomeNonDeserializerClass.class) on a property, or an AnnotationIntrospector.findDeserializer() override returning Class<?>.class where the class is not a ValueDeserializer (e.g. returning a serializer class, a factory, or a plain POJO by mistake).
Common situations: Copy-paste errors wiring @JsonDeserialize(as = ...) instead of @JsonSerialize(as = ...); returning the wrong Class reference from a custom introspector; returning a Class<MySerializer> where a deserializer was expected; classpath confusion after a refactor that renamed/moved classes.
Related errors
- AnnotationIntrospector returned key deserializer definition
- AnnotationIntrospector returned Class ${deserClass.getName()
- AnnotationIntrospector returned Converter definition of type
- Failed to parse Date value '%s': %s
- Cannot pass null Deserializers
AI-assisted analysis of FasterXML/jackson-databind@87876ca5c0 (2026-08-11).
Data as JSON: /api/errors/816c4dc5794b6b28.
Report an issue: GitHub.