惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

J
Java Code Geeks
Last Week in AI
Last Week in AI
T
Tailwind CSS Blog
WordPress大学
WordPress大学
B
Blog RSS Feed
T
The Blog of Author Tim Ferriss
F
Fortinet All Blogs
aimingoo的专栏
aimingoo的专栏
MongoDB | Blog
MongoDB | Blog
博客园 - Franky
C
Check Point Blog
P
Proofpoint News Feed
H
Help Net Security
月光博客
月光博客
博客园_首页
Stack Overflow Blog
Stack Overflow Blog
博客园 - 三生石上(FineUI控件)
Martin Fowler
Martin Fowler
Recent Announcements
Recent Announcements
人人都是产品经理
人人都是产品经理
U
Unit 42
美团技术团队
I
InfoQ
A
About on SuperTechFans

TanStack Blog

TanStack + Vercel Partnership | TanStack Blog TanStack AI Enters the RC Phase | TanStack Blog Inside a TanStack Router Navigation | TanStack Blog Form v2 is here: All you need to know about the alpha | TanStack Blog Announcing TanStack Table V9 | TanStack Blog TanStack Has a New Look | TanStack Blog Introducing TanStack Markdown and TanStack Highlight | TanStack Blog We Removed React Server Components from TanStack.com | TanStack Blog We Stopped Using RSC on TanStack.com | TanStack Blog Inside TanStack Table V9 Reactivity | TanStack Blog Run Any Coding Agent in a Sandbox, With One chat() Call | TanStack Blog TanStack Start and TanStack AI Win 2026 Open Source Awards | TanStack Blog How an Underrated Refactor Saved 90% Memory Usage | TanStack Blog TypeScript Performance in TanStack Table V9 | TanStack Blog TanStack AI Beta: The Switzerland of AI Tooling Grows Up | TanStack Blog TanStack Table V9: Taking Form | TanStack Blog TanStack AI: Your MCP, your way | TanStack Blog TanStack Start Adds First-Class Rsbuild Support | TanStack Blog Introducing Experimental Workflows and Orchestrators in TanStack AI | TanStack Blog Chat UIs Are Lists Until They Aren't | TanStack Blog Structured Output That Remembers Across Turns | TanStack Blog TanStack Virtual just got a lot faster, and finally handles iOS | TanStack Blog TanStack AI now fully speaks AG-UI | TanStack Blog Hardening TanStack After the npm Compromise | TanStack Blog Postmortem: TanStack npm supply-chain compromise | TanStack Blog Who Owns the Tree? RSC as a Protocol, Not an Architecture | TanStack Blog TanStack AI Just Learned to Compose Music | TanStack Blog Your AI Tool Calls Should Fail at Compile Time, Not in Production | TanStack Blog One Flag, Every Chunk: Debug Logging Lands in TanStack AI | TanStack Blog How We Test TanStack AI Across 7 Providers on Every PR | TanStack Blog
Stop Waiting on JSON: Stream Structured Output with One S...
Alem Tuzlak · 2026-05-14 · via TanStack Blog

by Alem Tuzlak on May 14, 2026.

Stop Waiting on JSON

You ask an LLM for a Person object. You hit send. The spinner spins. Five seconds. Ten. Twenty. Somewhere on a server in Oregon, the model is happily generating tokens, and your user is staring at a loading state until the very last } of the JSON arrives.

That UX is bad and you already know it. The fix, in theory, is "just stream it." The fix, in practice, has been writing 15 lines of glue: an onChunk handler, a useState, a parsePartialJSON call, a manual cast to your Person type. Repeat in every project. Hope you got the types right.

This release kills the glue. useChat({ outputSchema }) now gives you a typed partial and final straight from the hook. One schema. End to end.

Until now, mixing streaming with outputSchema in @tanstack/ai looked something like this on the client:

This works, but every byte of it is something you didn't want to write:

  • You're manually accumulating a string buffer.
  • You're calling parsePartialJSON yourself and hoping it tolerates whatever half-JSON the model just emitted.
  • You're casting chunk.value.object to Person with no real proof it actually matches your schema.
  • You're doing this in every component that wants a typed live preview.

The schema lives on the server. The same schema would happily describe partial and final on the client. There was no reason for the client to be guessing.

Pass the schema. Get the types back.

That's the whole thing.

  • partial is DeepPartial<Person> and updates on every streamed delta. The framework parses the partial JSON for you and narrows the type to whatever fields have arrived so far.
  • final is Person | null. It flips to a fully-typed Person the moment the model emits the structured-output completion event.
  • No parsePartialJSON import. No onChunk. No useState. No casts.

The same hook returns the message stream you already use, so partial UI previews and chat transcripts live side-by-side without conflict.

The headline is the hook, but the work that made it possible touches the whole stack.

A real type for the structured-output stream. chat({ outputSchema, stream: true }) now returns a StructuredOutputStream<T> that's a proper discriminated union: every regular StreamChunk plus a single tagged StructuredOutputCompleteEvent<T> carrying a strongly-typed value.object. You no longer fight any when you destructure or switch on the event.

Tagged variants for the other custom events too. While we were in there, ApprovalRequestedEvent and ToolInputAvailableEvent became their own tagged shapes. Tool-calling flows narrow cleanly without a helper.

Per-chunk debug logging in the structured path. The streaming structured output path now calls logger.provider on every chunk, matching the behavior of plain chatStream. Provider-level debugging is no longer a black box just because you opted into a schema.

Provider coverage. OpenAI, OpenRouter, Grok, Groq, and Ollama (anything riding the openai-base) all go through the same streaming structured output pipeline. The summarize adapter got the same treatment, so structured summaries stream end-to-end too.

Framework parity. useChat({ outputSchema }) works in @tanstack/ai-react, @tanstack/ai-vue, @tanstack/ai-solid, and @tanstack/ai-svelte. Same return shape, same types, same behavior.

There's a temptation, when designing this kind of API, to invent a separate hook: useStructuredChat, useTypedChat, something parallel. That would have been a mistake. A schema isn't a different mode of chatting, it's just extra information the hook can use.

By folding outputSchema into the existing useChat, the upgrade path from "I'm using a chat hook" to "I'm using a chat hook with typed streaming output" is literally one prop. Your tool wiring, your approval prompts, your transcript state, everything that was already on useChat still works exactly the same way. The new behavior only exists for the keys that depend on the schema.

The cost of "one more hook for a slightly different case" is paid in docs, in user confusion, and in the long tail of useFooButSometimesBar hooks people inevitably accumulate. We'd rather not.

Install the latest:

Define the schema once. Use it on both sides.

On the server, hand the schema to chat and stream the response as Server-Sent Events:

On the client, point useChat at that endpoint and pass the same schema:

Read the full guide: tanstack.com/ai/docs/chat/structured-outputs.

Drop the glue. Pass the schema. Ship the feature.