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
- Inspect the full exception stack to find which AnnotationIntrospector.findXxxConverter() method is on the stack and what it returns.
- Ensure that method returns either a Converter<?,?> instance or a Class<? extends Converter> (or Converter.None.class to signal 'none').
- If you intended to reference a converter by name, resolve the name to a Class (or instance) inside the introspector before returning it.
- 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
- Unit-test custom AnnotationIntrospector.findXxxConverter() methods to assert they only return Converter, Class<? extends Converter>, Converter.None.class, or null.
- Type your introspector's internal holder fields as Class<? extends Converter<?,?>> so the compiler catches mismatches.
- Audit mixins and introspectors during 2.x-to-3.x migration for raw Object converter returns.
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
- AnnotationIntrospector returned Class {}; expected Class<Con
- `AnnotationIntrospector` returned `Converter` definition of
- AnnotationIntrospector returned `Class<{}>`; expected `Class
- Invalid abstract type resolution from {} to {}: latter is no
- Failed to parse Date value '%s': %s
AI-assisted analysis of FasterXML/jackson-databind@a50c7d2a1d (2026-08-06).
Data as JSON: /api/errors/f92b9694b08c20bb.
Report an issue: GitHub.