lukasa1993/tanstack-skills-extracted/tanstack-ai-core-structured-outputs

Claude Code and agent skill by lukasa1993/tanstack-skills-extracted

Trust tier 2
  • community
LLM review pending
Source check pending

Checking that the SKILL.md file is still in the repository.

Type-safe JSON schema responses from LLMs using outputSchema on chat() and useChat(). Supports Zod, ArkType, and Valibot schemas. The adapter handles provider-specific strategies transparently — never configure structured output at the provider level. Pass stream:true alongside outputSchema for incremental JSON deltas + a completed typed object via the `structured-output.complete` event. Each successfully completed structured-output run adds a typed `StructuredOutputPart` to message history. pa…

Install

npx vskill@latest install lukasa1993/tanstack-skills-extracted/tanstack-ai-core-structured-outputs

SKILL.md

Structured Outputs

Dependency note: This skill builds on ai-core. Read it first for critical rules. The useChat patterns below build on ai-core/chat-experience — read that for the base hook surface, then come back here for the structured-output specifics.

Setup

typescript
import { chat } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { z } from 'zod'
const person = await chat({
adapter: openaiText('gpt-5.2'),
messages: [{ role: 'user', content: 'John Doe, 30' }],
threadId: 'thread-1',
outputSchema: z.object({
name: z.string(),
age: z.number(),
}),
})
person.name // string — fully typed, no cast
person.age // number

When outputSchema is provided, chat() returns Promise<InferSchemaType<TSchema>> instead of AsyncIterable<StreamChunk>. The result is fully typed.

Adding stream: true switches the return to StructuredOutputStream<InferSchemaType<TSchema>> — incremental JSON deltas plus a terminal validated object. See Pattern 3 below for direct iteration, Pattern 4 for the useChat shape on the client, Pattern 5 for multi-turn structured chats, and Pattern 6 for harness adapters.

Decision: which pattern fits

Building thisUse
One prompt in → one typed object out (script, server endpoint, CLI)Pattern 1 (basic) or 2 (nested)
A UI that fills in field by field as the model streams (progressive form, live card)Pattern 4 — useChat({ outputSchema })
Direct iteration of the stream in Node or testsPattern 3 — async iterable
Users iterate on a structured object across multiple turns (recipe builder, ticket refinement)Pattern 5 — multi-turn structured chat
Tools that gather info, then return a typed objectCombine any of the above with tools — see ai-core/tool-calling
A coding agent in a sandbox inspects files, then returns a typed objectPattern 6 — harness outputSchema

Core Patterns

Pattern 1: Basic structured output with Zod
typescript
import { chat } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { z } from 'zod'
const PersonSchema = z.object({
name: z.string().meta({ description: "The person's full name" }),
age: z.number().meta({ description: "The person's age in years" }),
email: z.string().email().meta({ description: 'Email address' }),
})
// chat() returns Promise<{ name: string; age: number; email: string }>
const person = await chat({
adapter: openaiText('gpt-5.2'),
messages: [
{
role: 'user',
content:
'Extract the person info: John Doe is 30 years old, email john@example.com',
},
],
threadId: 'thread-1',
outputSchema: PersonSchema,
})
console.log(person.name) // "John Doe"
console.log(person.age) // 30
console.log(person.email) // "john@example.com"
Pattern 2: Complex nested schemas
typescript
import { chat } from '@tanstack/ai'
import { anthropicText } from '@tanstack/ai-anthropic'
import { z } from 'zod'
const CompanySchema = z.object({
name: z.string(),
founded: z.number().meta({ description: 'Year the company was founded' }),
headquarters: z.object({
city: z.string(),
country: z.string(),
address: z.string().optional(),
}),
employees: z.array(
z.object({
name: z.string(),
role: z.string(),
department: z.string(),
}),
),
financials: z
.object({
revenue: z
.number()
.meta({ description: 'Annual revenue in millions USD' }),
profitable: z.boolean(),
})
.optional(),
})
const company = await chat({
adapter: anthropicText('claude-sonnet-4-5'),
messages: [
{
role: 'user',
content: 'Extract company info from this article: ...',
},
],
threadId: 'thread-1',
outputSchema: CompanySchema,
})
// Full type safety on nested properties
console.log(company.headquarters.city)
console.log(company.employees[0]?.role)
console.log(company.financials?.revenue)
Pattern 3: Direct stream iteration

Pass stream: true alongside outputSchema to get an async iterable of standard streaming chunks plus a completed typed object. Use this when you're a single process end-to-end — Node script, CLI, test, or a server endpoint that responds with one JSON blob. For the in-browser progressive-UI case, jump to Pattern 4 instead.

typescript group=person-stream
import { chat } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { z } from 'zod'
const PersonSchema = z.object({
name: z.string(),
age: z.number(),
email: z.string().email(),
})
const stream = chat({
adapter: openaiText('gpt-5.2'),
messages: [
{ role: 'user', content: 'Extract: John Doe is 30, john@example.com' },
],
threadId: 'thread-1',
outputSchema: PersonSchema,
stream: true,
})
for await (const chunk of stream) {
if (chunk.type === 'CUSTOM' && chunk.name === 'structured-output.complete') {
// Terminal event. `chunk.value.object` is complete and typed against the
// schema you passed in. Validate it in the consumer when required.
chunk.value.object.name // string
chunk.value.object.age // number
chunk.value.reasoning // string | undefined (thinking models only)
}
}

The terminal event is a CUSTOM chunk: { type: 'CUSTOM', name: 'structured-output.complete', value: { object: T, raw: string, reasoning?: string } }. The return type of chat({ outputSchema, stream: true }) carries T through, so a plain discriminated narrow (chunk.type === 'CUSTOM' && chunk.name === 'structured-output.complete') is enough — no type guard helper.

Adapter coverage for streaming:

AdapteroutputSchema + stream: true
@tanstack/ai-openai (Responses + Chat Completions)Native combined mode (#605) — schema wired into the regular chatStream call alongside tools; engine harvests JSON, no finalization round-trip
@tanstack/ai-anthropic (Claude 4.5+ only)Native combined mode (#605) — output_config.format + tools in one beta Messages call. Older Claude models fall back
@tanstack/ai-gemini (Gemini 3.x only)Native combined mode (#605) — responseSchema + tools in one generateContentStream. Gemini 2.x falls back
@tanstack/ai-grokNative combined mode (#605) — OpenAI Responses text.format + tools for grok-4.6, grok-4.5, grok-4.3, and grok-build-0.1
@tanstack/ai-openrouterNative single-request stream (legacy structuredOutputStream path; per-call combined-mode lookup is a follow-up)
@tanstack/ai-groqLegacy structuredOutputStream only (no tools — Groq's API rejects schema + tools + stream)
@tanstack/ai-bedrockSeparate native structuredOutputStream finalization through Converse or an OpenAI-compatible API
@tanstack/ai-byteplusNative combined mode on supported models; unsupported models emit RUN_ERROR
@tanstack/ai-cloudflareNative structuredOutputStream without tools; with tools, a separate finalization call (Workers AI models answer the tool turn in prose)
@tanstack/ai-claude-codeCombined + event source — --json-schema on the same harness turn. Read useChat().final. See Pattern 6.
@tanstack/ai-codexCombined + event source — --output-schema on the same harness turn. Read useChat().final. See Pattern 6.
@tanstack/ai-opencodeCombined + event source — prompt-and-parse. Read useChat().final. See Pattern 6.
@tanstack/ai-grok-buildCombined + event source — prompt-and-parse (ACP and streaming-json). Read useChat().final or the structured-output part. See Pattern 6.
@tanstack/ai-acp (acpCompatible)Combined + event source — prompt-and-parse. Read useChat().final or the structured-output part. See Pattern 6.

JSON mode (`json_object`). Some OpenRouter and Groq models reject strict json_schema. For those models the adapter sends json_object and adds the schema as a system prompt. The model is asked for the shape but is not held to it. A caller can force JSON mode with modelOptions (responseFormat: { type: 'json_object' } on OpenRouter, response_format on Groq and other OpenAI-base adapters). JSON mode is never combined with tools: the schema prompt goes only on the finalization call. Only await chat({ outputSchema }) with a Standard Schema validates the reply, so prefer Zod/Valibot/ArkType over a plain JSON Schema on these models.

Native-combined output vs separate finalization is signaled by the adapter's optional supportsCombinedToolsAndSchema(modelOptions) method. When it returns true, the engine wires the JSON Schema into the regular chatStream call and harvests the final-turn text — middleware sees the run through beforeModel / modelStream as usual, and the 'structuredOutput' middleware phase does not fire. When it returns false (or is omitted), the engine takes the legacy finalization path: agent loop, then a separate structuredOutput / structuredOutputStream call with 'structuredOutput' phase tagging.

Consumer code is identical across providers — always read the final object off structured-output.complete.

Pattern 4: useChat with outputSchema (progressive UI)

Pass outputSchema to useChat and you get a partial field that fills in as JSON streams in, plus a final field that snaps to the completed typed object on the terminal event. No onChunk ceremony, no manual JSON accumulation, no parsePartialJSON calls.

Server (same as Pattern 3, just behind an SSE endpoint):

typescript
// app/api/extract-person/route.ts (or your framework's equivalent)
import { chat, toServerSentEventsResponse } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { z } from 'zod'
const PersonSchema = z.object({
name: z.string(),
age: z.number(),
email: z.string().email(),
})

This is the start of the file. Read the full SKILL.md on GitHub.

Security

  • Security scanPassed, 100 of 100Pattern checks on the skill's files.
  • Model reviewNot runA model review is scheduled for the next rescan.

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

7-day trend score
0

Not refreshed yet.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/lukasa1993/tanstack-skills-extracted/tanstack-ai-core-structured-outputs/badge)](https://verified-skill.com/skills/lukasa1993/tanstack-skills-extracted/tanstack-ai-core-structured-outputs)

Something wrong with this skill or its page? Tell us.