FasterXML/jackson-databind · error · IllegalStateException

AnnotationIntrospector returned deserializer definition of…

Error message

AnnotationIntrospector returned deserializer definition of type ${deserDef.getClass().getName()}; expected type `ValueDeserializer` or `Class<ValueDeserializer>` instead

What it means

Thrown by DeserializationContextExt.deserializerInstance when an AnnotationIntrospector returned a deserializer definition whose runtime type is neither a ValueDeserializer instance nor a Class. Jackson expects @JsonDeserialize(using=...) (or the introspector equivalent) to resolve to a ValueDeserializer or a Class<ValueDeserializer>; anything else is treated as an introspector/annotation bug.

Solutions

  1. Fix the custom AnnotationIntrospector so findDeserializer returns a ValueDeserializer instance or a Class<? extends ValueDeserializer> (or ValueDeserializer.None, or null).
  2. Ensure @JsonDeserialize(using=X.class) points to a ValueDeserializer subclass and is not being rewritten by a mixin/introspector.
  3. Remove the introspector override that is rewriting deserializer definitions if it is incorrect.

Example fix

// before: introspector returns a String
@Override
public Object findDeserializer(Annotated a) {
    return a.getName();
}

// after: returns a Class<? extends ValueDeserializer>
@Override
public Object findDeserializer(Annotated a) {
    return MyValueDeserializer.class;
}
Defensive patterns

Strategy: type-guard

Validate before calling

Object def = introspector.findDeserializer(annotated);
if (def != null && !(def instanceof ValueDeserializer) && !(def instanceof Class)) {
    throw new IllegalStateException("Bad deserializer definition: " + def.getClass());
}

Type guard

boolean isDeserializerDef(Object d) {
    return d == null || d instanceof ValueDeserializer<?> || d instanceof Class;
}

Prevention

When it happens

Trigger: A custom AnnotationIntrospector's findDeserializer returns an arbitrary object (a String, Type, bean, etc.) instead of a ValueDeserializer or Class; a framework/annotation remapper leaking the wrong object into the deserializer slot.

Common situations: Writing a custom introspector and returning the wrong field; an annotation-driven framework that wraps @JsonDeserialize and returns a descriptor object instead of a class; upgrading Jackson where the introspector contract was tightened to disallow non-Class/non-ValueDeserializer returns.

Related errors


AI-assisted analysis of FasterXML/jackson-databind@87876ca5c0 (2026-08-11). Data as JSON: /api/errors/cad2ba2df6a527c4. Report an issue: GitHub.

Appendix: source

Thrown at src/main/java/tools/jackson/databind/deser/DeserializationContextExt.java:246

    /**********************************************************************
     */

    @SuppressWarnings("unchecked")
    @Override
    public ValueDeserializer<Object> deserializerInstance(Annotated ann, Object deserDef)
    {
        if (deserDef == null) {
            return null;
        }
        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());
            }
        }

View on GitHub (pinned to 87876ca5c0)