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:
// 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:
{"step":"search","status":"done"}
{"step":"summarize","status":"running"}
{"step":"summarize","status":"done"}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
titleyet — rendertitle ?? "". - 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
| Approach | Best for | Trade-off |
|---|---|---|
| Plain text streaming | Single long text field | No structure mid-stream |
| Tolerant partial parser | One growing structured object | Needs a library; re-parses |
| NDJSON | Many discrete events/steps | You control the producer |
| SSE deltas | Provider streaming APIs | Tied to the provider format |