Skip to content

loads() raises RecursionError on legal JSON5 nested just 60 levels deep #150

Closed

Description

@rahul05ranjan

While fuzzing json5.loads() against the standard library parser, I found that a legal JSON5 document nested just 60 levels deep crashes with RecursionError, while json.loads() handles the same shape to 400+ levels (and beyond).

Minimal repro (Python 3.13, pyjson5 from current main):

import sys, json, json5

print(sys.getrecursionlimit())  # 1000

json.loads('[' * 60 + ']' * 60)   # fine
json5.loads('[' * 59 + ']' * 59)  # fine
json5.loads('[' * 60 + ']' * 60)  # RecursionError

So a 120-byte input — quite reachable in real configs (nested schemas, AST-like documents, minified trees) — takes down the call instead of returning a value or a clean ValueError explaining the depth problem. Anyone parsing untrusted JSON5 gets a RecursionError rather than a parse error, which is easy to mishandle.

dumps() has the same flavor of problem, ~332 levels vs 2000+ for json.dumps():

o = []
for _ in range(332): o = [o]
json.dumps(o)    # fine
json5.dumps(o)  # RecursionError

I realize a hand-rolled recursive-descent parser (glop-generated, several frames per grammar level) will inherently use more stack than the C scanner in json — this reads more as "worth a guard rail" than a parsing bug. A couple of options, in case they're useful:

  • track nesting depth in the parser and fail with a normal ValueError("document nested too deeply") once the remaining stack budget gets thin (i.e., bail out before the interpreter does), or
  • document the effective depth limit as a known limitation in the README.

Happy to help with either if useful — thanks for the library, and for the fast merges on #141 :)

Activity

  1. dpranke commented on Nov 1, 2026

    @dpranke
    Owner

    Yeah, this is a known issue and is (roughly) documented in the README (see the third known issue). I don't document a specific limit because I think the limit depends on the kind of data being parsed. As the note says, I don't think this tends to come up in practice on real (non-synthetic/non-test) data.

    I thought about explicitly tracking the depth like you suggest, but I don't think it's worth it; it just turns a standard Python error (RecursionError) into a custom one. In addition, you can work around the standard limit w/ sys.setrecursionlimit if need be.

    It's possible I should be a bit more detailed on the note in the README (e.g., give an approximate depth, mention the workaround), though.

  2. rahul05ranjan commented on Nov 1, 2026

    @rahul05ranjan
    ContributorAuthor

    You're right — I missed that note in the Known Issues section, mea culpa for not reading the README thoroughly before filing. Fair call on the depth-guard trade-off too: turning a standard RecursionError into a custom error mostly helps the error message, not the caller.

    One small data point, take it or leave it: the input that surfaced this for me was machine-generated (an AST-ish document), so it does occasionally bite outside synthetic test data — files like that are where an approximate depth and the sys.setrecursionlimit workaround in that README note would save someone a confused afternoon. But that's just polish, and I appreciate the quick and thoughtful response either way. Closing this out.

  3. dpranke commented on Nov 1, 2026

    @dpranke
    Owner

    Interesting. Good to know that you hit this with real data.

    I will add to the readme note. I should probably document this in the docstrings as well ...

    Side note: the standard json module used to have more-or-less the same problem, although the level was much higher (something like 1000 levels), but it looks like they must've rewritten it in 3.12 because I haven't seen an upper bound since.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions