aillmjsonstreaming

Parsing Streaming (Partial) JSON from AI APIs

·8 min read·AI & JSON

The streaming problem

To make AI UIs feel fast, you stream the model's response token by token. But if the model is returning JSON, every intermediate chunk is invalid JSON{"title": "Hel will not parse. You cannot wait for the final token (that defeats streaming), and you cannot JSON.parse() a fragment. You need a strategy for partial JSON.

Strategy 1: Stream values, not structure

The simplest approach is to design the interaction so the streamed content is plain text and the structure is known in advance. For example, if you only need a single long answer field, stream raw text and wrap it yourself. Reserve JSON for the metadata you send at the end.

Strategy 2: Use a tolerant streaming parser

For genuinely structured streaming, use a parser built to handle incomplete input. These libraries "auto-close" open structures so a fragment becomes a valid partial object you can render immediately:

typescript
// Conceptually: complete the fragment, then parse
function parsePartial(fragment: string) {
  // close any open strings, objects, and arrays
  const completed = autoCloseJson(fragment);
  try { return JSON.parse(completed); }
  catch { return null; } // not enough yet — wait for more
}

Libraries such as partial-json, best-effort-json-parser, or framework helpers (for example the streaming object helpers in modern AI SDKs) implement this robustly. Each new chunk yields a more complete object, so your UI fills in progressively.

Strategy 3: Newline-delimited JSON (NDJSON)

If you control the producer, emit one complete JSON object per line (NDJSON / JSON Lines). Each line is independently parseable the moment its newline arrives:

text
{"step":"search","status":"done"}
{"step":"summarize","status":"running"}
{"step":"summarize","status":"done"}
typescript
const reader = response.body!.getReader();
const decoder = new TextDecoder();
let buffer = "";

for (;;) {
  const { value, done } = await reader.read();
  if (done) break;
  buffer += decoder.decode(value, { stream: true });
  let nl;
  while ((nl = buffer.indexOf("\n")) !== -1) {
    const line = buffer.slice(0, nl).trim();
    buffer = buffer.slice(nl + 1);
    if (line) handleEvent(JSON.parse(line)); // each line is valid JSON
  }
}

NDJSON is the most robust option for streaming structured events, which is why it underpins many agent and tool-streaming protocols.

Strategy 4: Server-Sent Events with JSON payloads

Provider streaming APIs typically send SSE frames, each carrying a small JSON delta. You parse each data: line as it arrives and accumulate the deltas. This is structurally similar to NDJSON — small, complete JSON messages rather than one giant growing object.

Rendering partial data safely

  • Guard every field. A partial object may not have title yet — render title ?? "".
  • Debounce re-renders. Parsing on every token is wasteful; parse every few chunks or on a short timer.
  • Show a streaming indicator until the final chunk so users know more is coming.

Choosing an approach

ApproachBest forTrade-off
Plain text streamingSingle long text fieldNo structure mid-stream
Tolerant partial parserOne growing structured objectNeeds a library; re-parses
NDJSONMany discrete events/stepsYou control the producer
SSE deltasProvider streaming APIsTied to the provider format

Frequently asked questions

No — individual chunks are almost always incomplete and will throw. Use a tolerant parser or a line-delimited format.

For streaming, yes. Independent lines mean each event is immediately parseable and a dropped line does not corrupt the rest.

They typically run a tolerant partial-JSON parser internally and emit progressively more complete objects to your callback as tokens arrive.

Try JSON Lines / NDJSON

Convert between JSON arrays and newline-delimited JSON for streaming.