FasterXML/jackson-databind · error · IllegalStateException

AnnotationIntrospector returned Converter definition of type

Error message

AnnotationIntrospector returned Converter definition of type {}; expected type Converter or Class<Converter> instead

What it means

During introspection, the AnnotationIntrospector returned a value for a @JsonSerialize/@JsonDeserialize 'converter' (or 'using') attribute that is neither a Converter instance nor a Class object. DatabindContext.converterInstance() can only resolve those two shapes, so any other type is a contract violation by the introspector. The message names the offending runtime class so you can identify which custom introspector logic produced the bad value.

Source

Thrown at src/main/java/tools/jackson/databind/DatabindContext.java:461

        return resolver;
    }

    /**
     * Helper method to use to construct a {@link Converter}, given a definition
     * that may be either actual converter instance, or Class for instantiating one.
     */
    @SuppressWarnings("unchecked")
    public Converter<Object,Object> converterInstance(Annotated annotated,
            Object converterDef)
    {
        if (converterDef == null) {
            return null;
        }
        if (converterDef instanceof Converter<?,?>) {
            return (Converter<Object,Object>) converterDef;
        }
        if (!(converterDef instanceof Class)) {
            throw new IllegalStateException("AnnotationIntrospector returned Converter definition of type "
                    +converterDef.getClass().getName()+"; expected type Converter or Class<Converter> instead");
        }
        Class<?> converterClass = (Class<?>)converterDef;
        // there are some known "no class" markers to consider too:
        if (converterClass == Converter.None.class || ClassUtil.isBogusClass(converterClass)) {
            return null;
        }
        if (!Converter.class.isAssignableFrom(converterClass)) {
            throw new IllegalStateException("AnnotationIntrospector returned Class "
                    +converterClass.getName()+"; expected Class<Converter>");
        }
        final MapperConfig<?> config = getConfig();
        HandlerInstantiator hi = config.getHandlerInstantiator();
        Converter<?,?> conv = (hi == null) ? null : hi.converterInstance(config, annotated, converterClass);
        if (conv == null) {
            conv = (Converter<?,?>) ClassUtil.createInstance(converterClass,
                    config.canOverrideAccessModifiers());
        }

View on GitHub (pinned to a50c7d2a1d)

Solutions

  1. Inspect the full exception stack to find which AnnotationIntrospector.findXxxConverter() method is on the stack and what it returns.
  2. Ensure that method returns either a Converter<?,?> instance or a Class<? extends Converter> (or Converter.None.class to signal 'none').
  3. If you intended to reference a converter by name, resolve the name to a Class (or instance) inside the introspector before returning it.
  4. Add a unit test asserting the introspector's converter methods only ever return those two types.

Example fix

// before
@Override
public Object findDeserializationConverter(Annotated a) {
    return a.getAnnotation(MyConvert.class).value(); // returns a String name
}
// after
@Override
public Object findDeserializationConverter(Annotated a) {
    String name = a.getAnnotation(MyConvert.class).value();
    if (name.isEmpty()) return null;
    try { return Class.forName(name); } // return a Class<? extends Converter>
    catch (ClassNotFoundException e) { return null; }
}
Defensive patterns

Strategy: type-guard

Validate before calling

// Validate an introspector's converter return before it is consumed
static Object safeConverterDef(Object def) {
    if (def == null) return null;
    if (def instanceof Converter<?,?>) return def;
    if (def instanceof Class<?> c && Converter.class.isAssignableFrom(c)) return def;
    throw new IllegalStateException("Bad converter def: " + def.getClass());
}

Type guard

boolean isConverterDef(Object o) {
    return o == null
        || o instanceof Converter<?,?>
        || (o instanceof Class<?> c && Converter.class.isAssignableFrom(c));
}

Prevention

When it happens

Trigger: A custom AnnotationIntrospector override (e.g. findSerializationConverter / findDeserializationConverter) returns a non-Converter, non-Class object such as a String, a Type, a Method, or a wrapper/holder bean. This typically only happens with hand-written introspectors or mixins that programmatically synthesize converter definitions.

Common situations: Migrating from 2.x where introspector return contracts were looser; building a dynamic introspector that looks up converters by name string and forgets to Class.forName()/instantiate them; passing a lambda or Supplier instead of the actual Converter instance; a custom annotation whose value type changed between versions being read raw into the converter slot.

Related errors


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