skip to content

How do you get a usable partial object out of a JSON response that is still streaming?

level: seniorimportance: should knowfreq 32%

answer

  1. JSON is only valid at the final brace
  2. track the open-delimiter stack
  3. close the copy, parse the copy
  4. closing delimiter means the value is final
  5. truncation and in-progress look identical

basics

~20 s

Buffer the tokens and run an incremental parser that tolerates an unterminated document — typically by closing any open string and brackets on a copy of the buffer, then parsing that. Only values whose closing delimiter has already arrived are trustworthy, and schema validation still runs once on the complete object.

solid answer

~50 s

A streamed JSON object is an incomplete document until the last brace arrives, so a standard parser rejects every intermediate state. Incremental parsing solves this by tracking the delimiter stack as bytes arrive and, on demand, producing a speculative complete document — close any open string, drop a dangling comma or key, then append the closing brackets the stack implies — and parsing that copy. What you get is a well-typed prefix of the final object. The discipline around it matters more than the trick: a value is only trustworthy once its closing delimiter has arrived, since a string can still be extended; never fire side effects from partial state; and the final object must still pass full schema and business-rule validation, because partial parsing proves nothing about validity. Crucially, a truncated response and an in-progress one look identical at the byte level, so you must distinguish them by the stream's own completion signal, not by whether the buffer happens to close.

code

python · 29 lines
python
import json

def parse_partial(buf: str):
    stack, in_str, esc = [], False, False
    for ch in buf:
        if in_str:
            if esc:
                esc = False
            elif ch == "\\":
                esc = True
            elif ch == '"':
                in_str = False
            continue
        if ch == '"':
            in_str = True
        elif ch in "{[":
            stack.append(ch)
        elif ch in "}]":
            stack.pop()
    patch = buf + ('"' if in_str else "")
    patch = patch.rstrip().rstrip(",")
    for ch in reversed(stack):
        patch += "}" if ch == "{" else "]"
    try:
        return json.loads(patch)
    except json.JSONDecodeError:
        return None

print(parse_partial('{"consignee": {"name": "Acme BV"}, "commodities": [{"hs_code": "8471'))

go deeper

for a junior

Know that a JSON object is not valid until its last brace arrives, so you cannot feed a stream to a normal parser field by field. Being able to say that partial parsing exists and requires a tolerant reader is enough here.

for a middle

Explain the mechanism — track the open-delimiter and in-string state, speculatively close a copy of the buffer, parse that — and state which values are stable versus still growing.

for a senior

Show the safety discipline: no side effects from partial state, completion determined by the stream's own signal rather than the buffer's shape, and full validation still run once on the finished object. Be ready to say when the complexity is not worth it.

for a principal

Own the decision of whether any consumer can genuinely act on a prefix. Frame it as a cost-of-complexity call against user-visible wait time, and be clear that partial parsing must never become a substitute for the validation boundary.

## Why the ordinary parser is not enough JSON is not a streaming-friendly format. It has no record separators and no self-delimiting frames; a document is valid only once the final closing brace arrives. So while a model is emitting a large object token by token, every intermediate buffer state is, by definition, invalid JSON. If your consumer waits for the complete document, nothing at all can happen for the duration of the generation — which for a fifty-line customs declaration can be many seconds of dead time even though the consignee block was fully determined in the first few hundred milliseconds. **Incremental (partial) parsing** is the technique that closes this gap: it turns a prefix of the byte stream into a well-typed partial value. ## How partial parsing works The common implementation is a small state machine over the buffer that tracks two things: whether the cursor is currently inside a string literal (respecting backslash escapes, so an escaped quote does not falsely close it), and a stack of open `{` and `[` delimiters. On each attempt, you build a **speculative document** on a copy of the buffer: 1. If the cursor is inside a string, append a closing quote. 2. Trim trailing whitespace and a dangling comma, and drop a trailing key or key-with-colon that has no value yet. 3. Append the closing delimiters implied by the stack, innermost first. Then parse the speculative document with an ordinary parser. If it succeeds, you hold a structurally sound prefix of the final object; if the buffer sits at an awkward boundary the parse fails and you simply wait for more bytes. The original buffer is never mutated — the speculative copy exists only to answer "what does this look like so far?" ## Partial-object semantics: what you may believe The subtle part is knowing which values in that prefix are stable. - A **scalar whose closing delimiter has arrived** — a string with its closing quote, a number followed by a comma or a brace — is final. - A **string still open** is a prefix, not a value. `"Amsterd` may become `"Amsterdam"` or `"Amsterdam Schiphol"`. Any logic keyed on that value can be wrong until the quote arrives. - A **completed object or array** is stable; an open one may still gain members. An array with three elements so far is not an array of three elements. - **Absence proves nothing.** A field missing from the prefix may simply not have been emitted yet. Emission order broadly follows the schema and prompt, but it is not a guarantee you should build logic on — treat the partial object as fill-in-as-they-arrive, not as ordered arrival. The operational rule that falls out: partial state may drive presentation, but must never drive **side effects**. Do not write a record, call an API, or make a routing decision from a prefix. Those wait for the complete, validated object. ## Truncation looks exactly like progress This is the failure mode that catches people. A buffer that stops mid-string because the model is still thinking and a buffer that stops mid-string because the generation hit its token limit are byte-for-byte indistinguishable. Partial parsing will happily produce a speculative object for either. So the completion signal must come from the transport and the response metadata — the stream's end-of-message event and the reason the generation stopped — not from the buffer's shape. When the stream ends, you assemble the *real* buffer (never the speculative one), parse it strictly, and run full schema plus business-rule validation. A response that stopped at the limit fails there and is handled as a truncation case rather than being quietly accepted because the speculative parse looked fine. ## Validation and streaming are separate concerns It is worth stating plainly: partial parsing is a **consumption** technique, not a validation technique. It tells you what has arrived; it says nothing about whether the final object satisfies your schema, whether required fields are present, or whether the values are semantically sound. All of that runs exactly once, on the complete document, unchanged from the non-streamed path. Teams that let a nice-looking partial parse substitute for the final check end up with unvalidated data in their systems. ## When it is worth the complexity Incremental parsing earns its keep when the object is large enough that the wait is user-visible and the early fields are independently meaningful — a document extraction where the header block is useful before the line items, or a long list where each item can be consumed as it lands. It is not worth it for a small object that completes in a few hundred milliseconds, and it is actively unhelpful when the consumer is a batch job that cannot act on anything until the whole record exists. As with most streaming work, the question is whether any consumer can do something useful with a prefix; if not, buffer and validate once.

  • Your partial parser returns a consignee name mid-string. What must the consumer not do with it?
    Anything irreversible or identity-bearing. An open string is a prefix, so "Acme" may still become "Acme Logistics BV" — matching it against a customer table, routing the shipment, or writing it anywhere durable can all be wrong. Presentation is the safe use: display it and let it update as more bytes arrive. Everything that changes state waits for the closing quote at minimum, and for the fully validated object in practice.
  • How do you tell a truncated response from one that is simply still arriving?
    Only from the transport and response metadata — the stream's end-of-message event and the stop reason the provider reports — never from the buffer's shape, because the two are byte-identical. Once the stream signals completion, you parse the real buffer strictly rather than the speculatively closed copy. If that parse or the schema check fails, you are in the truncation case, which is handled by raising the output allowance or decomposing the extraction, not by a repair prompt.
  • Does incremental parsing change how you validate?
    No. Validation runs once, on the complete document, with exactly the same schema and business rules as the non-streamed path. Partial parsing is a consumption convenience that tells you what has arrived so far; it makes no claim about required fields, types across the whole object, or semantic soundness. A common bug is treating a clean speculative parse as evidence of validity and skipping the real check.

saying these in an interview costs you the question

  • Thinks a standard parser can consume a JSON stream incrementally
  • Trusts a value whose closing delimiter has not arrived
  • Treats a field's absence in the prefix as a real absence
  • Assumes a buffer that stops mid-object means truncation
  • Skips final validation because the partial parse looked fine

context