Skip to content

Commit a866721

Browse files
authored
feat: add docs for structured outputs (#185)
1 parent f527119 commit a866721

File tree

2 files changed

+304
-0
lines changed

2 files changed

+304
-0
lines changed

docs/config.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@
5050
"label": "Agentic Cycle",
5151
"to": "guides/agentic-cycle"
5252
},
53+
{
54+
"label": "Structured Outputs",
55+
"to": "guides/structured-outputs"
56+
},
5357
{
5458
"label": "Streaming",
5559
"to": "guides/streaming"

docs/guides/structured-outputs.md

Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
---
2+
title: Structured Outputs
3+
id: structured-outputs
4+
order: 6
5+
---
6+
7+
Structured outputs allow you to constrain AI model responses to match a specific JSON schema, ensuring consistent and type-safe data extraction. TanStack AI uses the [Standard JSON Schema](https://standardschema.dev/) specification, allowing you to use any compatible schema library.
8+
9+
## Overview
10+
11+
When you provide an `outputSchema` to the `chat()` function, TanStack AI:
12+
13+
1. Converts your schema to JSON Schema format
14+
2. Sends it to the provider's native structured output API
15+
3. Validates the response against your schema
16+
4. Returns a fully typed result
17+
18+
This is useful for:
19+
20+
- **Extracting structured data** from unstructured text
21+
- **Building forms or wizards** with AI-generated content
22+
- **Creating APIs** that return predictable JSON shapes
23+
- **Ensuring type safety** between AI responses and your application
24+
25+
## Schema Libraries
26+
27+
TanStack AI uses **Standard JSON Schema**, which means you can use any schema library that implements the specification:
28+
29+
- [Zod](https://zod.dev/) (v4.2+)
30+
- [ArkType](https://arktype.io/)
31+
- [Valibot](https://valibot.dev/) (via `@valibot/to-json-schema`)
32+
- Plain JSON Schema objects
33+
34+
> **Note:** Refer to your schema library's documentation for details on defining schemas and using features like `.describe()` for field descriptions. TanStack AI will convert your schema to JSON Schema format automatically.
35+
36+
## Basic Usage
37+
38+
Here's how to use structured outputs with a Zod schema:
39+
40+
```typescript
41+
import { chat } from "@tanstack/ai";
42+
import { openaiText } from "@tanstack/ai-openai";
43+
import { z } from "zod";
44+
45+
// Define your schema
46+
const PersonSchema = z.object({
47+
name: z.string().describe("The person's full name"),
48+
age: z.number().describe("The person's age in years"),
49+
email: z.string().email().describe("The person's email address"),
50+
});
51+
52+
// Use it with chat()
53+
const person = await chat({
54+
adapter: openaiText("gpt-4o"),
55+
messages: [
56+
{
57+
role: "user",
58+
content: "Extract the person info: John Doe is 30 years old, email john@example.com",
59+
},
60+
],
61+
outputSchema: PersonSchema,
62+
});
63+
64+
// person is fully typed as { name: string, age: number, email: string }
65+
console.log(person.name); // "John Doe"
66+
console.log(person.age); // 30
67+
console.log(person.email); // "john@example.com"
68+
```
69+
70+
## Type Inference
71+
72+
The return type of `chat()` changes based on the `outputSchema` prop:
73+
74+
| Configuration | Return Type |
75+
|--------------|-------------|
76+
| No `outputSchema` | `AsyncIterable<StreamChunk>` |
77+
| With `outputSchema` | `Promise<InferSchemaType<TSchema>>` |
78+
79+
When you provide an `outputSchema`, TanStack AI automatically infers the TypeScript type from your schema:
80+
81+
```typescript
82+
import { z } from "zod";
83+
84+
// Define a complex schema
85+
const RecipeSchema = z.object({
86+
name: z.string(),
87+
prepTime: z.string(),
88+
servings: z.number(),
89+
ingredients: z.array(
90+
z.object({
91+
item: z.string(),
92+
amount: z.string(),
93+
})
94+
),
95+
instructions: z.array(z.string()),
96+
});
97+
98+
// TypeScript knows the exact return type
99+
const recipe = await chat({
100+
adapter: openaiText("gpt-4o"),
101+
messages: [{ role: "user", content: "Give me a recipe for scrambled eggs" }],
102+
outputSchema: RecipeSchema,
103+
});
104+
105+
// Full type safety - TypeScript knows all these properties exist
106+
recipe.name; // string
107+
recipe.prepTime; // string
108+
recipe.servings; // number
109+
recipe.ingredients[0].item; // string
110+
recipe.instructions.map((step) => step.toUpperCase()); // string[]
111+
```
112+
113+
## Using Field Descriptions
114+
115+
Schema field descriptions help the AI understand what data to extract. Most schema libraries support a `.describe()` method:
116+
117+
```typescript
118+
const ProductSchema = z.object({
119+
name: z.string().describe("The product name"),
120+
price: z.number().describe("Price in USD"),
121+
inStock: z.boolean().describe("Whether the product is currently available"),
122+
categories: z
123+
.array(z.string())
124+
.describe("Product categories like 'electronics', 'clothing', etc."),
125+
});
126+
```
127+
128+
These descriptions are included in the JSON Schema sent to the provider, giving the AI context about each field.
129+
130+
## Complex Nested Schemas
131+
132+
You can define deeply nested structures:
133+
134+
```typescript
135+
const CompanySchema = z.object({
136+
name: z.string(),
137+
founded: z.number().describe("Year the company was founded"),
138+
headquarters: z.object({
139+
city: z.string(),
140+
country: z.string(),
141+
address: z.string().optional(),
142+
}),
143+
employees: z.array(
144+
z.object({
145+
name: z.string(),
146+
role: z.string(),
147+
department: z.string(),
148+
})
149+
),
150+
financials: z
151+
.object({
152+
revenue: z.number().describe("Annual revenue in millions USD"),
153+
profitable: z.boolean(),
154+
})
155+
.optional(),
156+
});
157+
158+
const company = await chat({
159+
adapter: anthropicText("claude-sonnet-4-5"),
160+
messages: [
161+
{
162+
role: "user",
163+
content: "Extract company info from this article: ...",
164+
},
165+
],
166+
outputSchema: CompanySchema,
167+
});
168+
169+
// Access nested properties with full type safety
170+
console.log(company.headquarters.city);
171+
console.log(company.employees[0].role);
172+
```
173+
174+
## Combining with Tools
175+
176+
Structured outputs work seamlessly with the agentic tool loop. When both `outputSchema` and `tools` are provided, TanStack AI will:
177+
178+
1. Execute the full agentic loop (running tools as needed)
179+
2. Once complete, generate the final structured output
180+
3. Return the validated, typed result
181+
182+
```typescript
183+
import { chat, toolDefinition } from "@tanstack/ai";
184+
import { z } from "zod";
185+
186+
const getProductPrice = toolDefinition({
187+
name: "get_product_price",
188+
description: "Get the current price of a product",
189+
inputSchema: z.object({
190+
productId: z.string(),
191+
}),
192+
}).server(async ({ input }) => {
193+
// Fetch price from database
194+
return { price: 29.99, currency: "USD" };
195+
});
196+
197+
const RecommendationSchema = z.object({
198+
productName: z.string(),
199+
currentPrice: z.number(),
200+
reason: z.string(),
201+
});
202+
203+
const recommendation = await chat({
204+
adapter: openaiText("gpt-4o"),
205+
messages: [
206+
{
207+
role: "user",
208+
content: "Recommend a product for a developer",
209+
},
210+
],
211+
tools: [getProductPrice],
212+
outputSchema: RecommendationSchema,
213+
});
214+
215+
// The AI will call the tool to get prices, then return structured output
216+
console.log(recommendation.productName);
217+
console.log(recommendation.currentPrice);
218+
console.log(recommendation.reason);
219+
```
220+
221+
## Using Plain JSON Schema
222+
223+
If you prefer not to use a schema library, you can pass a plain JSON Schema object:
224+
225+
```typescript
226+
import type { JSONSchema } from "@tanstack/ai";
227+
228+
const schema: JSONSchema = {
229+
type: "object",
230+
properties: {
231+
name: { type: "string", description: "The person's name" },
232+
age: { type: "number", description: "The person's age" },
233+
},
234+
required: ["name", "age"],
235+
};
236+
237+
const result = await chat({
238+
adapter: openaiText("gpt-4o"),
239+
messages: [{ role: "user", content: "Extract: John is 25 years old" }],
240+
outputSchema: schema,
241+
});
242+
243+
// Note: With plain JSON Schema, TypeScript infers `unknown` type
244+
// You'll need to cast or validate the result yourself
245+
const person = result as { name: string; age: number };
246+
```
247+
248+
> **Note:** When using plain JSON Schema, TypeScript cannot infer the return type. The result will be typed as `unknown`. For full type safety, use a schema library like Zod.
249+
250+
## Provider Support
251+
252+
Structured outputs are supported by all major providers through their native APIs:
253+
254+
| Provider | Implementation |
255+
|----------|---------------|
256+
| OpenAI | Uses `response_format` with `json_schema` |
257+
| Anthropic | Uses tool-based extraction |
258+
| Google Gemini | Uses `responseSchema` |
259+
| Ollama | Uses JSON mode with schema |
260+
261+
TanStack AI handles the provider-specific implementation details automatically, so you can use the same code across different providers.
262+
263+
## Best Practices
264+
265+
1. **Use descriptive field names and descriptions** - This helps the AI understand what data to extract
266+
267+
2. **Keep schemas focused** - Extract only the data you need; simpler schemas produce more reliable results
268+
269+
3. **Use optional fields appropriately** - Mark fields as optional when the data might not be present in the source
270+
271+
4. **Validate edge cases** - Test with various inputs to ensure the schema handles edge cases correctly
272+
273+
5. **Use enums for constrained values** - When a field has a limited set of valid values, use enums:
274+
275+
```typescript
276+
const schema = z.object({
277+
status: z.enum(["pending", "approved", "rejected"]),
278+
priority: z.enum(["low", "medium", "high"]),
279+
});
280+
```
281+
282+
## Error Handling
283+
284+
If the AI response doesn't match your schema, TanStack AI will throw a validation error:
285+
286+
```typescript
287+
try {
288+
const result = await chat({
289+
adapter: openaiText("gpt-4o"),
290+
messages: [{ role: "user", content: "..." }],
291+
outputSchema: MySchema,
292+
});
293+
} catch (error) {
294+
if (error instanceof Error) {
295+
console.error("Structured output failed:", error.message);
296+
}
297+
}
298+
```
299+
300+
The error will include details about which fields failed validation, helping you debug schema mismatches.

0 commit comments

Comments
 (0)