ComposioHQ/composio · error · JSONSchemaRefResolutionError
JSON Schema $ref chain exceeded depth cap ({MAX_REF_CHAIN_DE
Error message
JSON Schema $ref chain exceeded depth cap ({MAX_REF_CHAIN_DEPTH}): {ref} What it means
A chain of $ref → $ref → $ref longer than MAX_REF_CHAIN_DEPTH (100) was hit while inlining references. Like the node-depth cap, it guards against cyclic or billion-laughs-style reference bombs and raises JSONSchemaRefResolutionError instead of blowing the recursion limit.
Source
Thrown at python/composio/utils/json_schema.py:211
if node_id in visiting:
return _cycle_break_sentinel()
visiting.add(node_id)
try:
ref = node.get("$ref")
ref = ref if isinstance(ref, str) else None
# External refs and non-``$ref`` nodes both pass through the same
# reflective clone path.
if ref is None or not ref.startswith("#"):
if ref is not None:
logger.warning("Leaving external $ref untouched: %s", ref)
cloned: t.Dict[str, t.Any] = {}
for key, value in node.items():
cloned[key] = walk(value, visited_refs, chain_depth, node_depth + 1)
return cloned
if chain_depth >= MAX_REF_CHAIN_DEPTH:
raise JSONSchemaRefResolutionError(
f"JSON Schema $ref chain exceeded depth cap "
f"({MAX_REF_CHAIN_DEPTH}): {ref}",
meta={"ref": ref},
)
if ref in visited_refs:
return _cycle_break_sentinel()
resolution = _try_resolve_pointer(root, ref)
if resolution.ok:
target: t.Any = resolution.value
elif on_unresolved == "sentinel":
if on_replace is not None and resolution.reason is not None:
on_replace(ref, resolution.reason)
target = {
**_cycle_break_sentinel(),
"description": UNRESOLVED_REF_DESCRIPTION,
}
else:View on GitHub (pinned to 64b1b85502)
Solutions
- Check meta['ref'] in the error to find the offending reference and break the cycle
- Rewrite cyclic schemas using bounded recursion or drop the cycle
- Reject untrusted schemas before processing
Defensive patterns
Strategy: try-catch
Validate before calling
import json
def ref_chain_exceeds(schema, cap=100):
defs = schema.get('$defs', schema.get('definitions', {}))
def chain(name, seen):
if name in seen: return True # cycle
target = defs.get(name)
if not isinstance(target, dict): return False
nxt = target.get('$ref')
if isinstance(nxt, str) and nxt.startswith('#/$defs/'):
return chain(nxt.split('/')[-1], seen | {name})
return False
return any(chain(k, set()) for k in defs) Try / catch
from composio.exceptions import JSONSchemaRefResolutionError
try:
out = dereference_json_schema(schema)
except JSONSchemaRefResolutionError as e:
if 'ref' in getattr(e, 'meta', {}):
defs = schema.setdefault('$defs', {})
defs.pop(e.meta['ref'].split('/')[-1], None)
out = dereference_json_schema(schema, on_unresolved="sentinel") Prevention
- Break $ref cycles when generating schemas from recursive types
- Treat very long ref chains in third-party specs as a red flag
When it happens
Trigger: A schema where following $ref links (each hop increments chain_depth) exceeds 100 hops — typically a reference cycle that the visited_refs set didn't break (e.g. refs parameterized differently) or a deliberately chained bomb schema.
Common situations: Cyclic $defs (A refs B, B refs A) that slip past cycle detection; adversarial schemas; schemas generated from mutually recursive types.
Related errors
- JSON Schema node depth exceeded cap ({MAX_NODE_DEPTH})
- Cannot resolve $ref {pointer}
- JSON Schema nesting too deep to dereference
- Invalid property type {p_type}: {schema!r}
- JSON Schema node depth exceeded cap (${MAX_NODE_DEPTH})
AI-assisted analysis of ComposioHQ/composio@64b1b85502 (2026-08-28).
Data as JSON: /api/errors/f2f34ce4a131054e.
Report an issue: GitHub.