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

  1. Check meta['ref'] in the error to find the offending reference and break the cycle
  2. Rewrite cyclic schemas using bounded recursion or drop the cycle
  3. 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

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


AI-assisted analysis of ComposioHQ/composio@64b1b85502 (2026-08-28). Data as JSON: /api/errors/f2f34ce4a131054e. Report an issue: GitHub.