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
- Validate that every @ref in the payload has exactly one earlier @id on a Map value, and remove duplicates.
- Avoid calling resolveForwardReferences with ids you did not obtain from the current UnresolvedForwardReference handle.
- Drop @JsonIdentityInfo on the Map value type if forward references are not required.
- 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
- Ensure JSON producers emit @id before any @ref for Map values.
- Do not mix hand-written @ref ids with Jackson-managed identity state.
- Round-trip test cyclic Map graphs and assert no UnresolvedForwardReference escapes.
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
- Trying to resolve a forward reference with id
- Argument `allowedSchemes` must not be null
- Cannot deserialize Singleton container from "+size+" entries
- Could not resolve Object Id
- Could not resolve Object Id
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)