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

  1. Verify the class referenced by @JsonDeserialize(as = ...) or returned by your AnnotationIntrospector actually implements tools.jackson.databind.ValueDeserializer.
  2. If you meant to customize serialization, move the annotation to @JsonSerialize(using = ...)/@JsonSerialize(as = ...).
  3. Return an instance (ValueDeserializer) instead of a Class, or return ValueDeserializer.None.class to signal 'no deserializer'.
  4. 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

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


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