Reading — step 1 of 5
Read
Recursive Structures
JSON's grammar is recursive with no depth bound: a value may contain values, ad infinitum. Your parser mirrors that with mutual recursion — parseValue → parseArray → parseValue — which is elegant right up until someone feeds you:
[[[[[[[[[[ ... 100000 deep ... ]]]]]]]]]]
Each nesting level is a stack frame. Python's default recursion limit is 1000 (sys.getrecursionlimit()); a document of 1500 nested arrays — 3001 bytes of input — kills a naive recursive parser with a RecursionError. That is not a ValueError, so a try/except built for parse errors won't catch it: the process just dies. This is a classic denial-of-service vector against services that parse untrusted JSON, and RFC 8259 explicitly blesses the defense: "An implementation may set limits on the maximum depth of nesting."
Real parsers pick one of two defenses:
- Bound the depth. Track the current nesting level; reject documents past a cap (Jackson's default
StreamReadConstraintscaps at 1000, SQLite's JSON functions at 1000, many C parsers lower). Cheap, simple, and what you will do here. - Eliminate recursion. Rewrite the parser as an explicit loop with its own stack of partially built containers. Memory still grows with depth, but heap is gigabytes where the C stack is megabytes — and the failure mode becomes a clean error instead of a crash.
Production parsers layer more on top — streaming APIs (ijson, Jackson streaming) that never hold the whole tree, SIMD scanners like simdjson — but depth-bounding is the part every safe parser shares.
Counting depth correctly
Define depth as the number of containers you are inside; the outermost container is depth 1, and scalars add nothing. The clean implementation: increment when entering { or [, check the cap immediately, decrement when leaving:
The trap is the off-by-one. If you increment after recursing, or start the outermost container at 0 and check with >=, you either reject the legal depth-5 document or accept the illegal depth-6 one. Anchor yourself with the boundary pair: [1, [2, [3, [4, [5, 6]]]]] is depth 5 (allowed); wrap it once more and it is depth 6 (rejected). And remember to decrement on close — siblings do not accumulate: [[1], [2], [3]] is depth 2, no matter how many sibling containers appear.
Your exercise
Parse each line with MAX_DEPTH = 5. In-bounds documents print their Python repr; too-deep documents print exactly ERR depth exceeded — the harness converts any ValueError whose message contains depth into that line, so raise ValueError("depth exceeded") from your enter check. The grader-caught mistake is the boundary: [[[[[1]]]]] (five brackets) must parse and print [[[[[1]]]]], while [[[[[[1]]]]]] (six) must print ERR depth exceeded — if either flips, you have the off-by-one above. Scalars are legal whole documents here: 1 prints 1 and "hello" prints 'hello' (repr quoting), with no depth involved.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…