FasterXML/jackson-databind · error · IllegalArgumentException
Invalid abstract type resolution from {} to {}: latter is no
Error message
Invalid abstract type resolution from {} to {}: latter is not a subtype of former What it means
DeserializationConfig.mapAbstractType() walks the registered AbstractTypeResolvers to remap an abstract type to a concrete one, and requires each step to produce a true subtype of the source type. When a resolver returns a type whose raw class is NOT assignable to the previous type, Jackson throws this IllegalArgumentException because the mapping would break type safety. The message names both the source type and the (invalid) target so you can see exactly which mapping is wrong.
Source
Thrown at src/main/java/tools/jackson/databind/DeserializationConfig.java:600
* @since 3.0
*/
public JavaType mapAbstractType(JavaType type)
{
if (!hasAbstractTypeResolvers()) {
return type;
}
// first, general mappings
while (true) {
JavaType next = _mapAbstractType2(type);
if (next == null) {
return type;
}
// Should not have to worry about cycles; but better verify since they will invariably occur... :-)
// (also: guard against invalid resolution to a non-related type)
Class<?> prevCls = type.getRawClass();
Class<?> nextCls = next.getRawClass();
if ((prevCls == nextCls) || !prevCls.isAssignableFrom(nextCls)) {
throw new IllegalArgumentException("Invalid abstract type resolution from "+type+" to "+next+": latter is not a subtype of former");
}
type = next;
}
}
/**
* Method that will find abstract type mapping for specified type, doing a single
* lookup through registered abstract type resolvers; will not do recursive lookups.
*/
private JavaType _mapAbstractType2(JavaType type)
{
Class<?> currClass = type.getRawClass();
for (AbstractTypeResolver resolver : abstractTypeResolvers()) {
JavaType concrete = resolver.findTypeMapping(this, type);
if ((concrete != null) && !concrete.hasRawClass(currClass)) {
return concrete;
}
}View on GitHub (pinned to a50c7d2a1d)
Solutions
- In your AbstractTypeResolver.findTypeMapping, guarantee the returned JavaType's raw class is assignable from (subtype of) the input raw class.
- If you genuinely need an incompatible target, you need a custom ValueDeserializer or @JsonDeserialize(as=...) on a compatible type, not an abstract type remap.
- Audit all registered AbstractTypeResolvers (SimpleModule.getAbstractTypeResolvers / mapper's module list) and check each mapping's assignability with type.isAssignableFrom(concrete).
- Add a test that calls config.mapAbstractType(...) for each mapping and asserts the result is a subtype.
Example fix
// before
@Override
public JavaType findTypeMapping(DeserializationConfig config, JavaType type) {
if (type.getRawClass() == Animal.class) {
return config.getTypeFactory().constructType(Vehicle.class); // Vehicle not an Animal!
}
return null;
}
// after
@Override
public JavaType findTypeMapping(DeserializationConfig config, JavaType type) {
if (type.getRawClass() == Animal.class) {
return config.getTypeFactory().constructType(Dog.class); // Dog extends Animal
}
return null;
} Defensive patterns
Strategy: validation
Validate before calling
// Verify an AbstractTypeResolver mapping is type-safe before registering
static JavaType safeMap(DeserializationConfig cfg, AbstractTypeResolver r, JavaType from) {
JavaType to = r.findTypeMapping(cfg, from);
if (to != null && !from.getRawClass().isAssignableFrom(to.getRawClass())) {
throw new IllegalStateException(from + " -> " + to + " is not a subtype");
}
return to;
} Type guard
boolean isValidAbstractMap(JavaType from, JavaType to) {
return to != null && from.getRawClass().isAssignableFrom(to.getRawClass());
} Try / catch
try {
mapper.readValue(json, AbstractType.class);
} catch (IllegalArgumentException e) {
if (e.getMessage().contains("not a subtype of former")) {
// log and surface the offending resolver mapping
}
throw e;
} Prevention
- Unit-test each AbstractTypeResolver.findTypeMapping() asserting the returned type is assignable to the input.
- Audit chained resolvers; each step must preserve assignability.
- Prefer @JsonDeserialize(as=ConcreteSubtype.class) on compatible types over broad abstract remaps.
When it happens
Trigger: Registering an AbstractTypeResolver (or Module that adds one, e.g. via SimpleModule.addAbstractTypeResolver / setAbstractTypeResolver) whose findTypeMapping returns an unrelated concrete type; a custom mixin-style remapping that maps e.g. List -> ArrayList is fine but Map -> SomeBean is not; two resolvers chained so the second receives an already-remapped type it was not written for.
Common situations: Migrating type mappings from 2.x where assignability checks were less strict; copying an AbstractTypeResolver from another project where the type hierarchy differed; using a resolver to 'swap' types for legacy compatibility without ensuring the target extends/implements the source; an accidental resolver registration left over from removed classes.
Related errors
- Failed to parse Date value '%s': %s
- Cannot handle managed/back reference '{}': type: value deser
- Type {} not Java Enum type
- Missing generic type information for ${type}
- AnnotationIntrospector returned Converter definition of type
AI-assisted analysis of FasterXML/jackson-databind@a50c7d2a1d (2026-08-06).
Data as JSON: /api/errors/aee1c523e878fb46.
Report an issue: GitHub.