FasterXML/jackson-databind · error · IllegalArgumentException

Trying to resolve a forward reference with id [" + id + "]…

Error message

Trying to resolve a forward reference with id [" + id + "] that wasn't previously seen as unresolved.

What it means

MapDeserializer.MapReferringAccumulator.resolveForwardReference is the Map analog of the Collection forward-reference accumulator. When @JsonIdentityInfo is used on Map values, unresolved ids are recorded; resolving an id that was never registered (already resolved, unknown, or duplicate) throws IllegalArgumentException. The id is included in the message.

Solutions

  1. Validate that every @ref in the payload has exactly one earlier @id on a Map value, and remove duplicates.
  2. Avoid calling resolveForwardReferences with ids you did not obtain from the current UnresolvedForwardReference handle.
  3. Drop @JsonIdentityInfo on the Map value type if forward references are not required.
  4. Use a custom DeserializationProblemHandler to skip or log unknown refs during tolerant parsing.

Example fix

// before
@JsonIdentityInfo(generator=ObjectIdGenerators.PropertyGenerator.class, property="@id")
public class Cache { public Map<String, Item> items; }
// JSON references @ref:"x9" but no @id:"x9" exists

// after: ensure every @ref has a matching earlier @id, or remove @JsonIdentityInfo
Defensive patterns

Strategy: validation

Validate before calling

// Before resolving, confirm the id is in the Map accumulator's pending set.
Object idToResolve = ...;
boolean known = pendingMapReferringIds.contains(idToResolve);
if (!known) { log.warn("Unknown Map forward-ref id {} ignored", idToResolve); }
else { accumulator.resolveForwardReference(ctxt, idToResolve, value); }

Try / catch

try {
    mapper.readValue(json, new TypeReference<Map<String,Entity>>(){});
} catch (UnresolvedForwardReference e) {
    Iterator<ReadableObjectId> it = e.iterator();
    while (it.hasNext()) { ReadableObjectId r = it.next(); /* resolve only reported ids */ }
}

Prevention

When it happens

Trigger: Deserializing JSON with @JsonIdentityInfo on a Map value type where a "@ref" key references an id never declared as unresolved; manually invoking resolveForwardReferences with an id not present in the pending accumulator; cyclic Map values whose JSON was tampered with.

Common situations: Bi-directional relationships stored in Map<K, Entity> with @JsonIdentityInfo where the producer emits a @ref before the matching @id or emits duplicate @ref entries; importing JSON from another serializer that does not honor Jackson identity semantics; partial deserialization that drops the first occurrence.

Related errors


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

Appendix: source

Thrown at src/main/java/tools/jackson/databind/deser/jdk/MapDeserializer.java:1036

            throws JacksonException
        {
            Iterator<MapReferring> iterator = _accumulator.iterator();
            // Resolve ordering after resolution of an id. This means either:
            // 1- adding to the result map in case of the first unresolved id.
            // 2- merge the content of the resolved id with its previous unresolved id.
            Map<Object,Object> previous = _result;
            while (iterator.hasNext()) {
                MapReferring ref = iterator.next();
                if (ref.hasId(id)) {
                    iterator.remove();
                    previous.put(ref.key, value);
                    previous.putAll(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 map. Called when the bound item
         * is rebound (e.g., builder → built object).
         *
         * @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) {
            replaceInMap(_result, oldItem, newItem);
            // Pending accumulator entries may also hold the old item if a later
            // forward ref hasn't yet resolved.
            for (MapReferring ref : _accumulator) {
                replaceInMap(ref.next, oldItem, newItem);

View on GitHub (pinned to 87876ca5c0)