本页还没有译文
本页 28 段还没译成日本語,下面显示的是英文原文。Structured Outputs Overview
还没翻译译自英文(tanstack.com)本页 28 段都还没译
Structured outputs constrain model responses to match a JSON Schema you control. You get back a typed object — not a string you have to parse, not a "mostly-JSON" blob you have to guess at, not a regex match. The model either returns something that fits your schema or you get a typed error.
You wire this in once with outputSchema. The runtime takes care of the rest: it converts your schema to JSON Schema, hands it to the provider's native structured-output API, validates the response, and infers the TypeScript type from your schema definition.
import { chat } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
import { z } from "zod";
const Person = z.object({ name: z.string(), age: z.number() });
const person = await chat({
adapter: openaiText("gpt-5.5"),
messages: [{ role: "user", content: "John Doe, 30" }],
outputSchema: Person,
});
person.name; // string — fully typed, no cast
person.age; // numberSchema Libraries
TanStack AI accepts any library that implements Standard JSON Schema:
- Zod (v4.2+)
- ArkType
- Valibot (via
@valibot/to-json-schema) - Plain JSON Schema objects (loses TypeScript type inference — see One-Shot Extraction)
Refer to your schema library's docs for field descriptions, refinements, and enums. TanStack AI converts the schema to JSON Schema automatically.
Provider Support
Every adapter handles structured output through its provider's native API:
| Provider | Implementation |
|---|---|
| OpenAI | response_format with json_schema |
| Anthropic | Tool-based extraction |
| Google Gemini | responseSchema |
| Ollama | JSON mode with schema |
| OpenRouter / Grok / Groq | response_format with json_schema |
| Claude Code / Codex | Native schema flag on the same harness turn (--json-schema / --output-schema) |
| OpenCode / Grok Build / ACP compatible | Same-turn prompt-and-parse |
The provider-specific details are handled for you. The same chat({ outputSchema }) call works across all of them. For a coding agent in a sandbox, see Harness Agents.
Anthropic schema complexity limits
Anthropic compiles a structured-output schema into a grammar and rejects schemas it considers too large or too complex with a 400 error — typically Schema is too complex for compilation or output_config.format.schema: Invalid schema: The compiled grammar is too large. This affects Claude models directly and anthropic/* models routed through OpenRouter, even when every other provider accepts the same schema.
If you hit one of these errors, simplify the schema. Complexity is driven less by overall size than by constructs that multiply the grammar's branching — common offenders:
.optional()fields you don't strictly need.catch()/.default()wrappers (they widen the accepted input)- Union types and deeply nested objects
- Unconstrained optional strings — tighten with enums or formats where possible
Anthropic's exact limits change over time and aren't all published, so we deliberately don't reproduce the numbers here — see Anthropic's structured outputs documentation for the current limits and reduction strategies.
Which page do I read?
Pick the journey that matches what you're building. The guides under Structured Outputs cover separate use cases. Read the one that fits.
| You want to… | Read |
|---|---|
Extract one structured object from a single prompt — and consume it server-side (script, endpoint, CLI) or in a browser via final | One-Shot Extraction |
| Build a UI that fills in field-by-field as the model streams (progressive form, live card, typewriter preview) | Streaming UIs |
| Walk through a Start app that fills a table from streamed rows | Streaming Structured Table |
| Let users iterate on a structured object across multiple turns — each turn produces a new typed object and history stays renderable | Multi-Turn Chat |
| Combine structured output with tool calls (agent loop that runs tools first, then returns a typed object) | With Tools |
| Ask a coding agent in a sandbox to inspect files, then return a typed object | Harness Agents |
The streaming and multi-turn paths both build on useChat({ outputSchema }). The "with tools" path layers on top of either. Pick the one that describes your shipping shape — start there, follow the cross-links when you need a piece of another story.
Note: Server-side validation is path-dependent. For the non-streaming agentic path (
await chat({ outputSchema })), the engine runs Standard Schema validation inside the finalization step and routes failures throughonError(the awaited promise rejects). For the streaming path (chat({ outputSchema, stream: true })), Standard Schema validation is deliberately deferred to the consumer — consumers read the object from thestructured-output.completeevent'svalue.objectfield (or callparseWithStandardSchemathemselves on the raw text). The schema you pass touseChat({ outputSchema })on the client is used for TypeScript inference and (inuseChat) for client-sideparsePartialJSON-based progressive parsing — the typed-object guarantee comes from the server-side path you pick.On both paths the engine normalizes the captured object before it reaches you: to satisfy strict providers, optional fields are widened to
required+ nullable, so the provider returnsnullfor an absent optional. The engine undoes exactly that widening — an.optional()field that came backnullreads back as absent (matchingT | undefined), while a genuine.nullable()field'snullis preserved. Sovalue.object(streaming) and the awaited result (non-streaming) both carry the un-widened shape your schema describes.
Middleware integration
Middleware configured on chat() now observes the final structured-output
provider call in addition to the agent loop. Chunks from the structured-output
adapter are attributed to ctx.phase === 'structuredOutput'; onFinish fires
exactly once at the end of the entire run.
Path-dependent: Adapters that natively combine
tools+ a schema- constrained final answer in one streaming call do not issue a separate finalization round-trip. The engine wiresoutputSchemainto the regularchatStreamrequest and harvests the structured result from the agent loop's final-turn text. On this path the'structuredOutput'middleware phase does not fire — middleware sees the run through'beforeModel'/'modelStream'as usual, andonStructuredOutputConfigis not invoked.Native combined providers:
- Modern OpenAI (Chat Completions + Responses)
- Anthropic Claude 4.5+
- Gemini 3.x
- Grok 4.x family
- OpenRouter, when every routed model is in
OPENROUTER_COMBINED_TOOLS_AND_SCHEMA_MODELS(see the OpenRouter adapter)Adapters without native combined-mode support (Anthropic 4.4-, Gemini 2.x, Grok 2/3, Groq, Ollama, and OpenRouter models outside that set) keep the legacy finalization path and the
'structuredOutput'phase fires as before.
Observing structured-output chunks
import { chat } from "@tanstack/ai";
import type { ChatMiddleware } from "@tanstack/ai";
import { span } from "./span";
const tracing: ChatMiddleware = {
name: "tracing",
onChunk(ctx, chunk) {
// Fires for chunks from the agent loop AND the final structured-output call
// chunk.type narrows inside a conditional — use a discriminant check first:
if ("type" in chunk) {
span.addEvent("chunk", { phase: ctx.phase, type: chunk.type });
}
},
};Transforming the JSON Schema before the provider call
Use the onStructuredOutputConfig hook when you need to mutate the schema:
import type { ChatMiddleware } from "@tanstack/ai";
import { sharedDefs } from "./schema-defs";
const injectDefs: ChatMiddleware = {
name: "inject-defs",
onStructuredOutputConfig(_ctx, config) {
return {
outputSchema: { ...config.outputSchema, $defs: { ...sharedDefs } },
};
},
};See Advanced: Middleware for the full hook reference.