FasterXML/jackson-databind · error · IllegalArgumentException

Trying to resolve a forward reference with id [${id}] that w

Error message

Trying to resolve a forward reference with id [${id}] that wasn't previously seen as unresolved.

What it means

Thrown by CollectionDeserializer.CollectionReferringAccumulator.resolveForwardReference when an object identity (from @JsonIdentityInfo) is being resolved against a Collection, but the supplied id was never registered as an unresolved forward reference. Jackson only records ids that it could not immediately satisfy during parsing; resolving any other id is a programmer/input error. It surfaces as an IllegalArgumentException because the accumulator's pending list does not contain the id.

Source

Thrown at src/main/java/tools/jackson/databind/deser/jdk/CollectionDeserializer.java:659

        public void resolveForwardReference(DeserializationContext ctxt, Object id, Object value) throws JacksonException
        {
            Iterator<CollectionReferring> iterator = _accumulator.iterator();
            // Resolve ordering after resolution of an id. This mean either:
            // 1- adding to the result collection in case of the first unresolved id.
            // 2- merge the content of the resolved id with its previous unresolved id.
            Collection<Object> previous = _result;
            while (iterator.hasNext()) {
                CollectionReferring ref = iterator.next();
                if (ref.hasId(id)) {
                    iterator.remove();
                    previous.add(value);
                    previous.addAll(ref.next);
                    return;
                }
                previous = ref.next;
            }

            throw new IllegalArgumentException("Trying to resolve a forward reference with id [" + id
                    + "] that wasn't previously seen as unresolved.");
        }

        /**
         * Replace a resolved item in the result collection. Called when the bound
         * item is rebound (e.g., builder → built object) via
         * {@link Referring#handleItemRebind}.
         *
         * @param oldItem Item to replace (Builder)
         * @param newItem Item to replace {@code oldItem} with (Built value)
         *
         * @since 3.2
         */
        public void replaceResolvedItem(Object oldItem, Object newItem) {
            if (_result instanceof List<?>) {
                @SuppressWarnings("unchecked")
                List<Object> list = (List<Object>) _result;
                for (int i = 0, len = list.size(); i < len; i++) {

View on GitHub (pinned to 87876ca5c0)

Solutions

  1. Inspect the JSON for the @ref whose id does not match any earlier @id in the same Collection, and correct the producer.
  2. If you call resolveForwardReferences manually, ensure you only resolve ids returned by UnresolvedForwardReference iterators from the same deserialization pass.
  3. Disable @JsonIdentityInfo on the element type (or scope it to a property) if forward references are not required, switching to default inline serialization.
  4. Add a custom DeserializationProblemHandler to log the offending id and continue instead of aborting.

Example fix

// before
@JsonIdentityInfo(generator=ObjectIdGenerators.PropertyGenerator.class, property="@id")
public class Node { public List<Node> children; }
// JSON has {"@ref":42} but no prior {"@id":42}

// after: validate ids before resolving, or drop identity info
List<Node> nodes = mapper.readValue(json, new TypeReference<List<Node>>(){});
// ensure every @ref in the payload has a matching earlier @id
Defensive patterns

Strategy: validation

Validate before calling

// Before resolving, confirm the id is actually pending.
UnresolvedForwardReference ufr = ...;
Object idToResolve = ...;
boolean known = false;
for (ReadableObjectId roid : ufr) { if (roid.hasId(idToResolve)) { known = true; break; } }
if (!known) { log.warn("Unknown forward-ref id {} ignored", idToResolve); }
else { roid.tryResolve(...); }

Try / catch

try {
    mapper.readerFor(Node.class).withAttribute(...).readValue(json);
} catch (UnresolvedForwardReference e) {
    // do NOT blindly iterate; only resolve ids reported by e
    Iterator<ReadableObjectId> it = e.iterator();
    while (it.hasNext()) { ReadableObjectId r = it.next(); /* handle */ }
}

Prevention

When it happens

Trigger: Deserializing JSON with @JsonIdentityInfo on a Collection element type where a "@ref" points to an id that was already resolved, never declared, or is a duplicate; or manually calling objectMapper.takeValueAsSomething followed by resolveForwardReferences with a stale/incorrect id on a Collection-typed property.

Common situations: Cyclic object graphs serialized with @JsonIdentityInfo where the JSON payload has been hand-edited or generated by a producer that emits duplicate @ref entries; round-tripping data through a cache that strips the first occurrence of an object; upgrading from a version that silently ignored unknown refs to one that enforces them.

Related errors


AI-assisted analysis of FasterXML/jackson-databind@87876ca5c0 (2026-08-11). Data as JSON: /api/errors/a18310204a69ef2f. Report an issue: GitHub.