BerriAI/litellm · error · ValueError

JSON merge patch nesting exceeds the maximum depth of {_MAX_

Error message

JSON merge patch nesting exceeds the maximum depth of {_MAX_MERGE_DEPTH}

What it means

apply_json_merge_patch implements RFC 7386 merge for nested objects and enforces a hard recursion ceiling of _MAX_MERGE_DEPTH = 64. When a patch nests dicts more than 64 levels deep, it raises ValueError at the depth check. The proxy uses this for PATCH /team/{team_id} metadata merging, so overly nested team metadata hits this error.

Source

Thrown at litellm/proxy/common_utils/json_merge_patch.py:26

# realistic team-metadata shape but well below Python's stack limit, so a
# pathologically deep patch is rejected instead of overflowing the stack.
_MAX_MERGE_DEPTH: Final = 64


def apply_json_merge_patch(target: JsonValue, patch: JsonValue, _depth: int = 0) -> JsonValue:
    """Apply an RFC 7386 JSON Merge Patch to ``target`` and return the result.

    - a key absent from ``patch`` keeps its value in ``target``
    - a key mapped to ``null`` in ``patch`` is removed from the result
    - any other value overwrites, recursing into nested objects

    ``target`` is never mutated; a new value is returned. Raises ``ValueError``
    if ``patch`` nests deeper than ``_MAX_MERGE_DEPTH``.
    """
    if not isinstance(patch, dict):
        return patch
    if _depth >= _MAX_MERGE_DEPTH:
        raise ValueError(f"JSON merge patch nesting exceeds the maximum depth of {_MAX_MERGE_DEPTH}")

    base: Final = target if isinstance(target, dict) else {}
    preserved: Final = {key: value for key, value in base.items() if key not in patch}
    applied: Final = {
        key: apply_json_merge_patch(base.get(key), value, _depth + 1)
        for key, value in patch.items()
        if value is not None
    }
    return {**preserved, **applied}

View on GitHub (pinned to 77b7c6c40c)

Solutions

  1. Flatten the metadata before sending: store deep documents as a JSON string field or by dotted keys.
  2. Compute the nesting depth client-side and cap it below 64.
  3. If you control both sides, move large structured data out of team metadata into dedicated storage.

Example fix

# before — nested 100 levels deep
metadata = cur
for _ in range(100):
    cur["child"] = {}
    cur = cur["child"]
patch_team(team_id, metadata)

# after — store as string
patch_team(team_id, {"policy_blob": json.dumps(metadata)})
Defensive patterns

Strategy: validation

Validate before calling

def json_depth(value) -> int:
    if isinstance(value, dict):
        return 1 + max((json_depth(v) for v in value.values()), default=0)
    if isinstance(value, list):
        return 1 + max((json_depth(v) for v in value), default=0)
    return 0

MAX_DEPTH = 64
patch = {"metadata": deep_doc}
assert json_depth(patch) < MAX_DEPTH, f"depth {json_depth(patch)} >= {MAX_DEPTH}: flatten first"

Type guard

def is_merge_patch_depth_ok(patch: dict, max_depth: int = 64) -> bool:
    def d(v, cur):
        if cur >= max_depth:
            return False
        if isinstance(v, dict):
            return all(d(x, cur + 1) for x in v.values())
        if isinstance(v, list):
            return all(d(x, cur + 1) for x in v)
        return True
    return d(patch, 0)

Try / catch

try:
    resp = client.patch(f"/team/{team_id}", json={"metadata": deep_doc})
    resp.raise_for_status()
except HTTPError as e:
    if e.response.status_code == 500 and "maximum depth" in e.response.text:
        flat = {"doc_blob": json.dumps(deep_doc)}
        resp = client.patch(f"/team/{team_id}", json={"metadata": flat})
    raise

Prevention

When it happens

Trigger: PATCH /team/{team_id} with a metadata object nested deeper than 64 levels (e.g. machine-generated nested configs or recursive structures serialized into metadata). Depth is counted per nested dict level in the patch.

Common situations: Storing a deeply nested JSON document (IAM policy trees, recursive UI state) inside team metadata. Two systems that embed each other's payloads, growing depth over time.

Related errors


AI-assisted analysis of BerriAI/litellm@77b7c6c40c (2026-08-18). Data as JSON: /api/errors/4fc822c294300a01. Report an issue: GitHub.