Skip to content
Recursive Structures
step 1/5

Reading — step 1 of 5

Read

~2 min readContainers: Arrays & Objects

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 — parseValueparseArrayparseValue — 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:

  1. Bound the depth. Track the current nesting level; reject documents past a cap (Jackson's default StreamReadConstraints caps at 1000, SQLite's JSON functions at 1000, many C parsers lower). Cheap, simple, and what you will do here.
  2. 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:

python

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…