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

推荐订阅源

The GitHub Blog
The GitHub Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Microsoft Security Blog
Microsoft Security Blog
J
Java Code Geeks
S
SegmentFault 最新的问题
Apple Machine Learning Research
Apple Machine Learning Research
N
Netflix TechBlog - Medium
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
博客园_首页
宝玉的分享
宝玉的分享
Google DeepMind News
Google DeepMind News
B
Blog RSS Feed
Hugging Face - Blog
Hugging Face - Blog
量子位
Blog — PlanetScale
Blog — PlanetScale
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
阮一峰的网络日志
阮一峰的网络日志
D
Docker
罗磊的独立博客
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
云风的 BLOG
云风的 BLOG
IT之家
IT之家
MyScale Blog
MyScale Blog
Microsoft Azure Blog
Microsoft Azure Blog

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.