JSON.parse fails until the stream ends. What approaches exist for parsing partial JSON while a structured response streams?
Advertisement
🧩 Scenario
Concept Walkthrough
Why JSON.parse cannot help
JSON.parse needs a complete document. Mid-stream you have something like {"days":[{"title":"Jai. It is valid JSON with a few closing characters missing. You need a way to read it early.
Option 1: wait for the end
Collect the text and parse once. Simple and fully validated. Fine for small objects, poor for long lists.
Option 2: repair, then parse (on every chunk)
Keep an accumulated string. On each chunk, close any open string, array and object, then parse the result. {"days":[{"title":"Jai becomes {"days":[{"title":"Jai"}]}. This is what the AI SDK does. Its parsePartialJson helper tries a normal parse first, then a repaired parse, and returns a state such as successful-parse or repaired-parse. The useObject hook calls it on every chunk and only updates state when the parsed value actually changed, a bit like a useMemo with a deep equality check.
The catch: the last string field is often half finished. "Jai" is not a city. Render it as in-progress text, but do not put a half URL into an href or a half number into a calculation.
Option 3: stream complete elements
If the output is a list, you often care about whole items, not half items. With streamText and Output.array, the AI SDK's elementStream emits each element only when it is complete and validated against the element schema. You get "day 1 is ready" instead of "day 1 title is 3 characters long".
Option 4: change the format
Ask for one JSON object per line (often called JSON Lines). Split on newlines, JSON.parse each full line. Very simple to parse, but you lose provider-side schema enforcement for the whole document, so you validate each line yourself.
Validate at the end
Partial objects cannot match a schema with required fields. Type them as deeply partial (the SDK uses a DeepPartial type), render with optional chaining, and validate the final object. useObject does that final check and passes { object, error } to onFinish.
Key Code Explained
// app/api/itinerary/schema.ts
import { z } from 'zod';
export const daySchema = z.object({
title: z.string(),
items: z.array(z.string()),
});
export const itinerarySchema = z.object({ days: z.array(daySchema) });
// app/api/itinerary/route.ts
import { streamText, Output, createTextStreamResponse, toTextStream } from 'ai';
import { chatModel } from '@/lib/models';
import { itinerarySchema } from './schema';
export async function POST(req: Request) {
const { city }: { city: string } = await req.json();
const result = streamText({
model: chatModel,
output: Output.object({ schema: itinerarySchema }),
prompt: `Plan a 10 day trip starting in ${city}.`,
});
// Streams the raw JSON text. The client repairs and parses it.
return createTextStreamResponse({
stream: toTextStream({ stream: result.stream }),
});
}
// app/itinerary/page.tsx
'use client';
import { useObject } from '@ai-sdk/react';
import { itinerarySchema } from '../api/itinerary/schema';
export default function Itinerary() {
const { object, submit, isLoading, stop } = useObject({
api: '/api/itinerary',
schema: itinerarySchema, // used for the final check, not mid-stream
onFinish: ({ error }) => {
if (error) console.error('Final object failed validation', error);
},
});
return (
<div>
<button onClick={() => submit({ city: 'Jaipur' })}>Plan trip</button>
{isLoading && <button onClick={stop}>Stop</button>}
{object?.days?.map((day, i) => (
<section key={i}>
<h3>{day?.title ?? '...'}</h3>
<ul>
{day?.items?.map((item, j) => <li key={j}>{item}</li>)}
</ul>
</section>
))}
</div>
);
}
Every access uses ?. because any level may not exist yet. On the server, if you need partial objects yourself (for example to push progress into your own stream), iterate result.partialOutputStream.
Tradeoffs
| Approach | First paint | Validation | Complexity |
|---|---|---|---|
| Wait for full text | Slow | Full, once | Lowest |
| Repair and parse | Fastest | Only at the end | Low with an SDK |
| elementStream (arrays) | Fast | Each element validated | Low |
| JSON Lines | Fast | Per line, by you | Medium (prompt must hold the format) |
What Interviewers Actually Check
- Whether you know why
JSON.parsefails mid-stream - Whether you can name more than one approach and pick by use case
- Whether you handle the half-finished last field
- Whether you type partial data as deeply partial and render defensively
- Whether final validation still happens, and where
Follow-Up Questions
- How would you show a field as "still typing" versus "done"?
- What happens to keys in a list when items arrive one by one? Is the index safe here?
- How would you cancel the stream and keep what has arrived?
- How does re-parsing the whole buffer on every chunk scale for very long output?
- When would you rather stream text and a separate final object?
Common Candidate Mistakes
- Swallowing parse errors on each chunk and rendering nothing until the end
- Using regex to extract values from incomplete JSON
- Treating a partial string as final data
- Running full schema validation on every partial object
- Re-rendering on every chunk without checking if the value changed
Interview Readiness Checklist
Before you leave this question, make sure you can answer:
- Can you name four approaches to partial JSON and when each fits?
- Can you repair a partial JSON string by hand?
- Can you build the route and the
useObjectclient above from memory? - Can you explain what
elementStreamguarantees? - Can you say where the final validation happens?
Summary
JSON.parse needs a complete document, so streaming structured output needs another plan. You can wait for the end, repair and parse on every chunk, stream whole validated elements, or use one JSON object per line. The AI SDK covers the common cases: partialOutputStream and useObject for repaired partial objects, elementStream for complete array items. Partial data cannot be validated against the full schema, so render it with optional chaining, be careful with half-finished fields, and validate the final object once the stream ends.
Why not just wait for the full response?
For small objects that is often the right call. For long lists or reports, users stare at a spinner for many seconds. Streaming partial objects lets the first card appear early, which feels much faster.
Can I validate a partial object with Zod?
Not against the full schema, because required fields may not have arrived yet. The AI SDK docs say partial outputs cannot be validated. Render defensively during the stream and validate the final object.
Is re-parsing the whole text on every chunk slow?
It grows with the length of the text, so very long outputs do more work per chunk. Compare the result with the previous one and only update state when it changed, and throttle renders if needed.
Advertisement
Stay Updated
Get the latest frontend challenges, interview questions and tutorials delivered to your inbox.
Advertisement