Quick answer: When streaming JSON from AI APIs, treat incoming content as fragments of one document unless the API explicitly defines another format. Decode the transport, accumulate the JSON text, verify successful generation completion, and then parse and validate it. Progressive previews can improve the interface, but only validated final data should enter your application's committed state.
Your AI feature works during development. A user submits a short message, the model returns a JSON object, and your application saves it without trouble. Then you turn on streaming to make the interface feel faster. Suddenly, the same feature throws a JSON parsing error before the model has finished answering.
The payload may be perfectly fine when complete. The failure happens because your code tries to read a document while it is still arriving.
Imagine a support dashboard that extracts a ticket summary, priority, and suggested tags. Showing the summary as it appears is useful. Saving an unfinished priority or triggering a notification from a partially generated tag is a different decision. A reliable implementation makes that distinction explicit.
This guide focuses on that streaming boundary. If you already have a complete response with broken quotes or extra Markdown, start with the guide to fixing AI-generated JSON. Here, the main problem is timing: deciding when a response is ready to become application data.
Inspect complete individual records in the JSON Viewer, or use the JSONPath Query Tester to select fields from a parsed record. Neither tool replaces an incremental stream parser.
What does streaming JSON actually mean?
Streaming describes how data arrives, rather than changing the rules of JSON. An API can deliver one complete JSON document in several content fragments. Those fragments might look like this:
Fragment 1: {"summary":"Customer needs
Fragment 2: a refund","priority":
Fragment 3: "normal","tags":["billing"]}
Combined in order, they produce a valid document:
{
"summary": "Customer needs a refund",
"priority": "normal",
"tags": ["billing"]
}
Neither the first nor the second fragment is an independent object. A fragment can stop inside a quoted value, after a backslash, or halfway through a number. Network boundaries do not necessarily follow field boundaries, and model text boundaries do not necessarily follow network boundaries.
That explains why a parser can report unexpected end of JSON input even while the request is proceeding normally. The parser sees everything it was given; it has no knowledge that more characters are coming later.
Before implementing anything, identify the response format your provider actually sends. A byte stream, an event stream, and an SDK iterator are different interfaces. The right extraction step depends on which one you have.
Separate transport events from model-generated content
Many streaming APIs use event envelopes. An event may contain a type, an identifier, a text delta, or an error. The envelope describes the stream. The text inside it may describe your ticket, invoice, or report.
These are two separate parsing tasks. First decode the transport event using the SDK or the documented protocol. Then select the content fragment that belongs to the JSON result you are building. Concatenating whole event envelopes creates a different document from the one you asked the model to generate.
The same distinction applies to Server-Sent Events. A raw network chunk can contain half an event, several events, or part of a UTF-8 character. Do not split arbitrary network chunks into assumed complete JSON messages. Prefer a maintained SDK or a protocol-aware decoder that retains incomplete input between reads.
For an application using a provider SDK, keep the adapter small. Its job is to convert provider events into content deltas and an explicit terminal outcome. Your application's JSON parsing code can then remain independent of provider-specific event names.
Read the provider's current streaming documentation before naming those events in production code. For a documented example of progressive structured responses, see TanStack AI's streaming structured-output guide. Its distinction between partial and final objects illustrates the interface boundary; verify behavior for your particular adapter rather than assuming every provider behaves identically.
The simplest safe option: accumulate, then parse
If your interface does not need individual fields before completion, collect the content fragments and parse once. This keeps the implementation easy to debug and avoids interpreting incomplete values.
The following JavaScript function expects an application-defined event iterator. It is intentionally not a direct replacement for any provider SDK. Your adapter must emit delta events for the intended JSON content and a complete event only after it has checked the provider's successful terminal outcome.
async function readFinalJson(events, maxCharacters = 100000) {
let text = "";
let completed = false;
for await (const event of events) {
if (event.type === "error") {
throw new Error("Generation failed");
}
if (event.type === "delta") {
if (completed || typeof event.text !== "string") {
throw new Error("Invalid stream event");
}
text += event.text;
if (text.length > maxCharacters) {
throw new Error("Response exceeds character limit");
}
} else if (event.type === "complete") {
completed = true;
}
}
if (!completed) {
throw new Error("Stream ended before successful completion");
}
return JSON.parse(text);
}
The character limit is an example application policy, not a provider limit or a recommended universal size. JavaScript string length measures UTF-16 code units; use a separate byte budget if your system requires one. Choose limits around your expected document sizes and cancel the upstream request when you abandon processing.
Notice that the function checks completion before parsing. A connection closing is not necessarily evidence of successful generation. Likewise, an object that happens to parse early is not necessarily the final result. The provider might still be sending content or reporting a refusal, truncation, or other terminal condition.
This function returns a parsed value, not a guaranteed ticket. JSON can represent arrays, strings, numbers, booleans, and null as well as objects. Follow it with a validator for the structure your feature needs.
Validate the completed result before saving it
A response can pass JSON parsing and still fail your application's contract. Consider these three issues: a missing summary, a priority outside the accepted choices, and a tags value that is a string instead of an array. None requires invalid JSON syntax.
For a small example, an explicit application validator is readable:
function validateTicket(value) {
if (value === null || typeof value !== "object" || Array.isArray(value)) {
throw new Error("Ticket must be an object");
}
const allowedKeys = ["summary", "priority", "tags"];
if (Object.keys(value).some((key) => !allowedKeys.includes(key))) {
throw new Error("Unexpected ticket field");
}
if (typeof value.summary !== "string" || value.summary.trim() === "") {
throw new Error("Summary must be a non-empty string");
}
if (!["low", "normal", "high"].includes(value.priority)) {
throw new Error("Invalid ticket priority");
}
if (!Array.isArray(value.tags) ||
!value.tags.every((tag) => typeof tag === "string")) {
throw new Error("Tags must be an array of strings");
}
return value;
}
For larger contracts, use a maintained validation library and a shared schema. The JSON Schema introduction explains how required fields and types fit together. The schema specification's object reference also clarifies how required and additionalProperties control missing and unexpected properties.
Keep business checks separate from structural checks. An allowed priority value does not establish that the model assigned the correct priority. A valid customer identifier does not establish that the current user may access that customer's records. Validate shape first, then check meaning, ownership, and permissions before changing persistent state.
When you need a progressive preview
Some interfaces benefit from displaying partial results. A long report can reveal section titles early. An extraction form can show fields as they become available. In these cases, use an incremental parser or the partial-object support provided by your framework.
Treat the preview as temporary UI state. Missing fields should remain missing rather than being silently filled with production defaults. A blank amount and a zero amount have different meanings. An incomplete list should not be presented as a confirmed total.
Your UI can show a clear status such as “Generating draft.” When the stream finishes successfully, parse or obtain the final object, validate it, and replace the preview with the accepted result. If validation fails, retain an error state and offer a retry instead of displaying the preview as a successful completion.
Avoid attaching irreversible actions to preview updates. Sending an email, creating an invoice, or changing an access role should depend on validated final data and your normal authorization rules. Progressive rendering should improve perceived responsiveness without changing the conditions under which your application commits a decision.
Check whether your selected framework validates the final object or merely infers a TypeScript type. Type information helps developers write code, but it cannot prove that a runtime response satisfies the schema. This is especially relevant when switching adapters: an interface that supports streaming may fall back to a single completed result with another provider.
Why counting braces is not a parser
It is tempting to track opening and closing braces and parse when their counts match. That approach breaks when text values contain braces:
{
"example": "Use {customer_name} in the template",
"message": "The closing brace } is part of this sentence"
}
Escaped quotes and backslashes add more complexity. A correct implementation must distinguish syntax from characters inside strings and retain that state across fragments. Once you add those rules, you are implementing a parser, with all the testing and maintenance that entails.
Do not append closing brackets to make a truncated response parse. That can turn a visibly failed generation into plausible but incomplete data. For example, closing an unfinished products array could silently omit products the model had not reached yet. The escape-character guide explains another reason that character-level repairs can change the meaning of a value.
When you cannot recover the intended content from a completed response, reject it or request a fresh result. A repair policy should be explicit, bounded, and appropriate for the data's consequences.
Streaming one object versus NDJSON
Not every streaming workflow needs a single large object. If each record is independent and the producer supports it, newline-delimited JSON can make incremental processing easier. Each line contains a complete JSON value, so a consumer can validate completed records individually.
{"ticketId":"T-101","priority":"normal"}
{"ticketId":"T-102","priority":"high"}
The two lines together are not one standard JSON document. They are two records in an NDJSON stream. Do not run one JSON.parse call over the entire block. Buffer incomplete lines, parse completed records, and define how the producer signals completion and handles a last line without a trailing newline.
This option is useful for logs or exports under your control. It does not automatically apply to an AI API that returns fragments of one schema-constrained object. Choose the format based on the actual protocol and downstream consumers. Read the JSON Lines and NDJSON guide before changing an existing endpoint's contract.
Handle cancellation, retries, and logging deliberately
A reliable feature needs more than a successful example. Users navigate away, requests time out, and a second generation may start before the first one ends. Give each request its own buffer and identity. Cancel the previous request when appropriate, and ignore late preview events from a request the interface has already replaced.
Retry only failures that your policy allows. A temporary network failure may justify another attempt. A schema bug in your own code will not improve through repeated generation. Set a maximum attempt count, show a useful failure message, and avoid performing the same downstream action twice when a request is retried.
Log enough context to classify the problem: request identifier, terminal status, response length, validation stage, and error category. Avoid storing complete customer messages or secrets by default. For manual investigation, create a harmless reproduction or redact sensitive JSON fields before sharing the payload.
Finally, distinguish the latency of the first visible preview from the time at which the complete result becomes usable. Streaming can make the interface respond sooner while the final validation still happens later. Measuring both helps you understand whether the added complexity improves the feature users actually experience.
Test the boundaries before releasing
Split the same known valid document at different positions and confirm that your accumulator produces the same final object. Include splits inside strings, between a backslash and its escape character, and around array delimiters. If you decode raw bytes, include non-ASCII characters split across byte chunks.
Exercise failure paths as well: an error after several deltas, an iterator that ends without successful completion, an oversized response, malformed final JSON, and valid JSON with the wrong structure. Verify that each failure leaves persistent state untouched and gives the user an appropriate next step.
Then test your preview separately. A missing field should not become a misleading default. A cancelled request should not overwrite a newer one. A visually complete card should still carry a generating state until the final result has passed validation.
For a completed payload, JSON Prettifier can help inspect syntax and nesting. It does not replace your transport decoder, incremental parser, schema validator, or business rules. Use each check at the boundary it is designed to protect.
A practical implementation checklist
- Identify whether the API sends bytes, transport events, SDK objects, or independent records.
- Use a supported decoder and extract only the intended JSON content.
- Keep buffers isolated by request and impose appropriate size limits.
- Display partial data only as a temporary preview when the feature needs it.
- Require a successful terminal outcome before accepting the result.
- Parse the completed document and validate its application contract.
- Check business rules and permissions before committing changes.
- Test interruption, cancellation, malformed output, and retry behavior.
Start with accumulation and final validation if that meets your needs. Add progressive field rendering when it solves a specific interface problem, and keep the same acceptance checks. That gives you a responsive AI feature whose finished data remains predictable enough for the rest of your application to use.