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

  1. In your AbstractTypeResolver.findTypeMapping, guarantee the returned JavaType's raw class is assignable from (subtype of) the input raw class.
  2. If you genuinely need an incompatible target, you need a custom ValueDeserializer or @JsonDeserialize(as=...) on a compatible type, not an abstract type remap.
  3. Audit all registered AbstractTypeResolvers (SimpleModule.getAbstractTypeResolvers / mapper's module list) and check each mapping's assignability with type.isAssignableFrom(concrete).
  4. 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

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


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