You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: docs/structured-outputs/streaming.md
+11-9Lines changed: 11 additions & 9 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -2,7 +2,7 @@
2
2
title: Streaming Structured Output UIs
3
3
id: structured-outputs-streaming
4
4
order: 3
5
-
description: "Build a UI that fills in field by field as the model streams structured JSON. chat({ outputSchema, stream: true }) on the server, useChat({ outputSchema }) on the client — progressive partial state plus a validated terminal object."
5
+
description: "Build a UI that fills in field by field as the model streams structured JSON. chat({ outputSchema, stream: true }) on the server, useChat({ outputSchema }) on the client — progressive partial state plus a typed terminal object."
6
6
keywords:
7
7
- tanstack ai
8
8
- structured outputs
@@ -16,7 +16,7 @@ keywords:
16
16
17
17
You have an existing chat-style endpoint and you want the structured response to populate a UI _while_ the model is generating — a form filling in field by field, a card whose ingredients list grows as JSON streams in, a typewriter preview of a JSON-typed report. Blocking on `await chat({ outputSchema })` would leave the UI dark until the whole object is ready; this guide is the alternative.
18
18
19
-
By the end you'll have a server endpoint streaming structured JSON as Server-Sent Events, and a client that reads a typed `partial` (progressive object) and `final` (validated terminal object) from `useChat`.
19
+
By the end you'll have a server endpoint streaming structured JSON as Server-Sent Events, and a client that reads a typed `partial` (progressive object) and `final` (completed terminal object) from `useChat`.
20
20
21
21
> **Note:** This is the streaming counterpart of [One-Shot Extraction](./one-shot). If you don't need progressive UI updates, the one-shot path is simpler. If you want users to iterate on the object across multiple turns and keep history, see [Multi-Turn Chat](./multi-turn).
22
22
@@ -48,11 +48,11 @@ export async function POST(request: Request) {
48
48
}
49
49
```
50
50
51
-
That's the entire server side. `chat({ outputSchema, stream: true })` returns a `StructuredOutputStream<InferSchemaType<typeof PersonSchema>>` — an `AsyncIterable` of standard streaming events plus a terminal `structured-output.complete` event carrying the validated object. `toServerSentEventsResponse` knows what to do with it.
51
+
That's the entire server side. `chat({ outputSchema, stream: true })` returns a `StructuredOutputStream<InferSchemaType<typeof PersonSchema>>` — an `AsyncIterable` of standard streaming events plus a terminal `structured-output.complete` event carrying the completed object. `toServerSentEventsResponse` knows what to do with it.
52
52
53
53
## Client with `useChat`
54
54
55
-
Pass the same schema to `useChat`. The hook gives you a progressively-parsed `partial` and a validated`final`:
55
+
Pass the same schema to `useChat`. The hook gives you a progressively-parsed `partial` and a typed`final`:
-**`partial`** is `DeepPartial<z.infer<typeof PersonSchema>>` — every property optional, every nested array element optional. Updated from `TEXT_MESSAGE_CONTENT` deltas via the runtime's partial-JSON parser. The hook derives it from the latest assistant message's `structured-output` part (see [Multi-Turn Chat](./multi-turn) for why that distinction matters), so it reads `{}` between `sendMessage()` and the first chunk without any extra reset state.
94
-
-**`final`** is `z.infer<typeof PersonSchema> | null` — the validated terminal payload from the `structured-output.complete` event. `null` until the run completes successfully.
95
-
-**`outputSchema`** is used purely for client-side TypeScript inference. Validation still runs on the server against the schema you pass to `chat({ outputSchema })` on the server route — the client doesn't re-validate.
94
+
-**`final`** is `z.infer<typeof PersonSchema> | null` — the completed terminal payload from the `structured-output.complete` event. `null` until the run completes successfully.
95
+
-**`outputSchema`** is used purely for client-side TypeScript inference. The streaming path does not run Standard Schema validation; validate the completed object in the consumer when required.
96
96
- The same shape works for **non-streaming adapters too**. If an adapter (Anthropic, Gemini, Ollama) returns a single `structured-output.complete` event with no incremental deltas, `partial` stays `{}` and `final` populates when the event arrives. Same consumer code.
97
97
98
98
`outputSchema` is optional: omit it and `useChat` returns its standard shape without `partial` / `final`.
@@ -144,7 +144,7 @@ return (
144
144
type: "CUSTOM",
145
145
name: "structured-output.complete",
146
146
value: {
147
-
object: T; //validated, parsed, typed
147
+
object: T; //completed, parsed, typed
148
148
raw: string; // full accumulated JSON text
149
149
reasoning?:string; // present only for thinking/reasoning models
150
150
},
@@ -164,6 +164,8 @@ Streaming structured output works with **every adapter**, but only some support
| Other adapters (anthropic, gemini, ollama, …) | Fallback: runs non-streaming `structuredOutput` and emits the final object as one `structured-output.complete` event |
168
170
169
171
The fallback path keeps the consumer code identical across providers — you always read the final object off `structured-output.complete` — but you won't see incremental deltas unless the adapter implements `structuredOutputStream` natively.
@@ -192,7 +194,7 @@ const stream = chat({
192
194
193
195
forawait (const chunk ofstream) {
194
196
if (chunk.type==="CUSTOM"&&chunk.name==="structured-output.complete") {
195
-
//Validated and typed against PersonSchema.
197
+
//Typed against PersonSchema. Validate here when required.
0 commit comments