FasterXML/jackson-databind · error · IllegalStateException

AnnotationIntrospector returned deserializer definition of t

Error message

AnnotationIntrospector returned deserializer definition of type {}; expected type `ValueDeserializer` or `Class<ValueDeserializer>` instead

What it means

Thrown by DeserializationContextExt.deserializerInstance when the object returned for a deserializer definition is neither a ValueDeserializer instance nor a Class. The introspector (or @JsonDeserialize(using=...)) must yield one of those two shapes (or null); anything else violates the contract.

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 a50c7d2a1d)

Solutions

  1. Audit the introspector's deserializer-returning methods: return null, a ValueDeserializer instance, or a Class<? extends ValueDeserializer>.
  2. If resolving handlers by name, resolve the actual Class first and return that.
  3. Add a contract unit test covering all code paths of the introspector method.

Example fix

// before
public Object findDeserializer(Annotated a) {
    return "myCustomDeserializer"; // String -> throws
}
// after
public Object findDeserializer(Annotated a) {
    return MyCustomDeserializer.class; // Class<? extends ValueDeserializer>
}
Defensive patterns

Strategy: type-guard

Type guard

static boolean isLegalDeserializerDef(Object def) {
    return def == null
        || def instanceof ValueDeserializer
        || (def instanceof Class<?> c && ValueDeserializer.class.isAssignableFrom(c));
}

Prevention

When it happens

Trigger: A custom AnnotationIntrospector.findDeserializer (or equivalent) returns a String, a Type, a Supplier, a bean-name, or another wrapper instead of a ValueDeserializer instance or Class<ValueDeserializer>.

Common situations: A custom framework introspection layer that wraps handler refs in its own types; a library upgrade where the introspector contract tightened; an annotation pointing at a bean name instead of a class.

Related errors


AI-assisted analysis of FasterXML/jackson-databind@a50c7d2a1d (2026-08-06). Data as JSON: /api/errors/011c1d6476373aac. Report an issue: GitHub.