FasterXML/jackson-databind · error · IllegalStateException

AnnotationIntrospector returned key deserializer definition

Error message

AnnotationIntrospector returned key deserializer definition of type {}; expected type KeyDeserializer or Class<KeyDeserializer> instead

What it means

Thrown by DeserializationContextExt.keyDeserializerInstance when the object returned for a key-deserializer definition is neither a KeyDeserializer instance nor a Class. The introspector 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:283

        // 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;
        }

        KeyDeserializer deser;

        if (deserDef instanceof KeyDeserializer keyDeserializer) {
            deser = keyDeserializer;
        } else {
            if (!(deserDef instanceof Class)) {
                throw new IllegalStateException("AnnotationIntrospector returned key deserializer definition of type "
                        +deserDef.getClass().getName()
                        +"; expected type KeyDeserializer or Class<KeyDeserializer> instead");
            }
            Class<?> deserClass = (Class<?>)deserDef;
            // there are some known "no class" markers to consider too:
            if (deserClass == KeyDeserializer.None.class || ClassUtil.isBogusClass(deserClass)) {
                return null;
            }
            if (!KeyDeserializer.class.isAssignableFrom(deserClass)) {
                throw new IllegalStateException("AnnotationIntrospector returned Class "+deserClass.getName()
                        +"; expected Class<KeyDeserializer>");
            }
            HandlerInstantiator hi = _config.getHandlerInstantiator();
            deser = (hi == null) ? null : hi.keyDeserializerInstance(_config, ann, deserClass);
            if (deser == null) {
                deser = (KeyDeserializer) ClassUtil.createInstance(deserClass,
                        _config.canOverrideAccessModifiers());
            }

View on GitHub (pinned to a50c7d2a1d)

Solutions

  1. Audit the introspector's key-deserializer-returning methods: return null, a KeyDeserializer instance, or a Class<? extends KeyDeserializer>.
  2. Resolve any name-based lookup to the actual Class before returning.
  3. Add a contract unit test for the introspector method.

Example fix

// before
public Object findKeyDeserializer(Annotated a) {
    return "colorKeyDeser"; // String -> throws
}
// after
public Object findKeyDeserializer(Annotated a) {
    return ColorKeyDeserializer.class; // Class<? extends KeyDeserializer>
}
Defensive patterns

Strategy: type-guard

Type guard

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

Prevention

When it happens

Trigger: A custom AnnotationIntrospector.findKeyDeserializer (or custom @JsonKeyDeserializer handling) returns a non-Class, non-KeyDeserializer object (a String, a Type, a wrapper).

Common situations: A custom introspection layer wrapping key-deserializer refs in its own types; an annotation pointing at a name/Type instead of a class; a library upgrade tightening the contract.

Related errors


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