Skip to content
Streaming Parser (SAX-style)
step 1/5

Reading — step 1 of 5

Read

~3 min readProduction Concerns

Streaming Parser (SAX-style)

json.loads is a tree-building parser: the whole document becomes one in-memory value. For a 20 GB log dump that is a death sentence. A streaming parser never builds the tree — it emits events as it scans, and the caller decides what to keep:

Input:  {"name": "alice", "age": 30}
Events: START_OBJECT
        KEY name
        STRING alice
        KEY age
        NUMBER 30
        END_OBJECT

The idea comes from XML's SAX API; JSON incarnations are ijson (Python), Jackson's streaming API (Java), yajl (C), and serde_json's streaming layer (Rust). What it buys you:

  • Bounded memory — gigabytes of input, constant RAM.
  • Early termination — found what you were looking for? Stop reading.
  • Pipelining — events can feed a downstream consumer before the document ends.
  • Validation-only — check well-formedness without materializing anything.

Two API styles exist: push (parser drives, calling your handler callbacks — SAX classic) and pull (you drive, asking for the next event — StAX, Python generators). You'll build pull: in Python a generator function that yields events mid-scan is the whole pattern.

The state you actually need

Streaming does not mean stateless. Two questions demand context the current character cannot answer:

  1. Is this string a KEY or a STRING value? In an object, strings alternate roles: member name, then member value. Keep a stack of contexts — push when you enter { or [, pop on close, and track within-object phase. A string is KEY exactly when the top context is an object expecting a member name.
  2. Is a comma legal here? Same stack answers it.

The stack holds one enum per nesting level — bytes, not the document. That is the trick: structure state is cheap; content is what streaming refuses to hold.

Errors cannot be unprinted

A tree parser that fails returns nothing. A streaming parser has already emitted events by the time it hits byte N — and that is a feature (downstream work on valid prefixes has already happened) with a sharp edge: consumers must be ready for a stream that ends in an error instead of clean closes. Your harness prints events as they arrive, so a malformed tail means good events followed by an ERR line. Design consumers for exactly that.

Your exercise

Emit one event per line, spelled exactly: START_OBJECT, END_OBJECT, START_ARRAY, END_ARRAY, KEY <name>, STRING <s>, NUMBER <n>, TRUE, FALSE, NULL. Scalars are whole documents: null prints NULL, 42 prints NUMBER 42, "hi" prints STRING hi. The KEY/STRING distinction is graded: {"name":"alice"} is KEY name then STRING alice — emit STRING name and you fail. The grader-caught streaming subtlety is the hidden test {"x":1,}: the expected output is the three good events START_OBJECT, KEY x, NUMBER 1 and then ERR object key must be a string — the events already printed stay printed (a stream cannot retract), and the error message here is exactly object key must be a string, because this parser discovers a } where the promised member name should be. Raise ValueError("object key must be a string") at that point and let the harness print it.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…