The Easiest Way to Generate Structured Output with the AI SDK
Learn how to use the AI SDK's structured output features to get reliable, type-safe responses from AI models. From simple text generation to complex object schemas, we'll cover everything you need to know.
The Easiest Way to Generate Structured Output with the AI SDK
Generating structured output from AI models has always been a challenge. You send a prompt, get back some text, and then spend time parsing it into the format you actually need. The AI SDK changes all of that with its powerful structured output features.
In this guide, we'll explore the different ways to generate structured output using the AI SDK, from simple text generation to complex, validated object schemas.
Why Structured Output Matters
Before diving into the how, let's understand the why:
- Type Safety: Get responses that match your expected data structure
- Reliability: No more parsing failures or unexpected formats
- Developer Experience: Better IntelliSense and error handling
- Production Ready: Structured output is essential for production AI applications
Method 1: Basic Text Generation with generateText
The simplest form of structured output is getting plain text responses. Here's how to use generateText
:
1import { generateText } from 'ai';
2import { google } from '@ai-sdk/google';
3
4const model = google("gemini-2.5-flash-lite");
5
6const result = await generateText({
7 model,
8 prompt: "Write a short summary of the benefits of structured output",
9 system: "You are a helpful AI assistant that writes concise summaries."
10});
11
12console.log(result.text); // The generated text
When to use: Simple text generation, creative writing, summaries, explanations.
Method 2: Structured Objects with generateObject
This is where the magic happens. generateObject
allows you to define a schema and get back a perfectly structured object:
1import { generateObject } from 'ai';
2import { z } from 'zod';
3import { google } from '@ai-sdk/google';
4
5const model = google("gemini-2.5-flash-lite");
6
7// Define your schema using Zod
8const UserProfileSchema = z.object({
9 name: z.string(),
10 age: z.number(),
11 interests: z.array(z.string()),
12 bio: z.string().max(200),
13 isActive: z.boolean()
14});
15
16const result = await generateObject({
17 model,
18 schema: UserProfileSchema,
19 prompt: "Create a user profile for a 25-year-old developer who loves TypeScript and AI",
20 system: "You are a data generation expert. Always return valid data matching the schema."
21});
22
23// result.object is fully typed and validated
24console.log(result.object.name); // TypeScript knows this is a string
25console.log(result.object.interests); // TypeScript knows this is string[]
Key Benefits:
- Automatic Validation: The AI SDK ensures the response matches your schema
- Type Safety: Full TypeScript support with IntelliSense
- Error Handling: Built-in validation and error reporting
Method 3: Advanced Schemas with Nested Objects
For complex applications, you can create sophisticated schemas:
1import { generateObject } from 'ai';
2import { z } from 'zod';
3
4const ChangeSchema = z.object({
5 title: z.string(),
6 content: z.string(),
7 type: z.enum(["added", "changed", "deprecated", "removed", "fixed", "security", "other"]),
8 size: z.enum(["minor", "patch", "major"]),
9 version: z.string(),
10 date: z.string()
11});
12
13const ChangelogEntrySchema = z.object({
14 version: z.string(),
15 date: z.string(),
16 changes: z.array(ChangeSchema).default([])
17});
18
19const result = await generateObject({
20 model,
21 schema: ChangelogEntrySchema,
22 prompt: "Analyze this commit and create a changelog entry",
23 system: "You are a changelog expert. Analyze commits and categorize changes appropriately."
24});
Method 4: Using Tools for Complex Operations
The AI SDK also supports tools, which are perfect for operations that require external data or complex processing:
1import { tool } from 'ai';
2import { z } from 'zod';
3
4// Define a tool for extracting context from URLs
5export const urlContext = tool({
6 description: "Extract context from a URL",
7 inputSchema: z.object({
8 url: z.string(),
9 }),
10 execute: async ({ url }) => {
11 const res = await fetch(url);
12 if (!res.ok) {
13 throw new Error(`Failed to fetch URL: ${res.status}`);
14 }
15 const text = await res.text();
16 return { text };
17 },
18});
19
20// Use the tool in your generation
21const result = await generateObject({
22 model,
23 schema: SummarySchema,
24 prompt: "Summarize the content from this URL",
25 tools: [urlContext]
26});
Real-World Example: Changelog Generation
Here's how we use structured output in ShipLog to analyze commits and generate changelog entries:
1// From our actual codebase
2export async function analyzeCommit(commit: CommitData, project: Project) {
3 const system = ChangelogEntryPrompt;
4
5 try {
6 const result = await generateObject({
7 model,
8 schema: ChangelogEntrySchema,
9 system,
10 prompt: `Analyze this commit and determine if it should be included in the changelog:
11
12 Commit Information:
13 ${JSON.stringify(commit, null, 2)}
14
15 Please analyze this commit and return a structured response.`
16 });
17
18 // result.object is guaranteed to match our schema
19 return result.object;
20 } catch (error) {
21 console.error("Error analyzing commit:", error);
22 return defaultAnalysis;
23 }
24}
Best Practices for Structured Output
1. Use Descriptive Schemas
1// ❌ Too generic
2const Schema = z.object({
3 data: z.any()
4});
5
6// ✅ Specific and descriptive
7const UserProfileSchema = z.object({
8 name: z.string().min(1, "Name is required"),
9 email: z.string().email("Invalid email format"),
10 preferences: z.object({
11 theme: z.enum(["light", "dark", "auto"]),
12 notifications: z.boolean()
13 })
14});
2. Provide Clear System Prompts
1const system = `You are a data generation expert. Your responses must:
2- Always match the provided schema exactly
3- Use realistic, appropriate data
4- Follow the specified format without deviation
5- Provide meaningful, useful information`;
3. Handle Errors Gracefully
1try {
2 const result = await generateObject({
3 model,
4 schema: MySchema,
5 prompt: "Generate user data"
6 });
7
8 return result.object;
9} catch (error) {
10 if (error.name === 'ZodValidationError') {
11 console.error('Schema validation failed:', error.issues);
12 return fallbackData;
13 }
14 throw error;
15}
4. Use Default Values for Optional Fields
1const Schema = z.object({
2 required: z.string(),
3 optional: z.string().optional().default("default value"),
4 array: z.array(z.string()).default([])
5});
Performance Considerations
1. Model Selection
Different models have different strengths:
- Fast & Cheap:
gemini-2.5-flash-lite
- Great for simple structured output - Balanced:
gpt-4-turbo
- Good balance of speed and quality - High Quality:
gpt-4
- Best for complex reasoning and structured output
2. Schema Complexity
Keep your schemas as simple as possible while meeting your needs:
1// ❌ Too complex - may cause generation issues
2const ComplexSchema = z.object({
3 nested: z.object({
4 deeply: z.object({
5 nested: z.object({
6 data: z.array(z.object({
7 // ... many more levels
8 }))
9 })
10 })
11 })
12});
13
14// ✅ Simpler, more reliable
15const SimpleSchema = z.object({
16 items: z.array(z.object({
17 id: z.string(),
18 value: z.string()
19 }))
20});
Common Pitfalls and How to Avoid Them
1. Schema Mismatch
Problem: AI generates data that doesn't match your schema Solution: Use clear prompts and validate your schema design
1// Make your schema intuitive
2const IntuitiveSchema = z.object({
3 firstName: z.string(), // Clear field name
4 lastName: z.string(),
5 fullName: z.string().optional() // Optional computed field
6});
2. Overly Strict Validation
Problem: Schema is too restrictive, causing generation failures Solution: Balance strictness with flexibility
1// ❌ Too strict
2const StrictSchema = z.object({
3 age: z.number().int().positive().max(120)
4});
5
6// ✅ More flexible
7const FlexibleSchema = z.object({
8 age: z.number().int().positive().max(120).default(25)
9});
3. Insufficient Context
Problem: AI doesn't understand what you want Solution: Provide clear examples and context
1const prompt = `Generate a user profile. Here are some examples:
2- A developer: {name: "Alice", age: 28, interests: ["coding", "AI"]}
3- A designer: {name: "Bob", age: 32, interests: ["UI/UX", "art"]}
4
5Now generate a profile for a data scientist.`;
Testing Your Structured Output
Always test your structured output generation:
1// Test function
2async function testGeneration() {
3 try {
4 const result = await generateObject({
5 model,
6 schema: TestSchema,
7 prompt: "Generate test data"
8 });
9
10 console.log('✅ Generation successful:', result.object);
11
12 // Validate the result matches your expectations
13 const validation = TestSchema.safeParse(result.object);
14 if (!validation.success) {
15 console.error('❌ Validation failed:', validation.error);
16 }
17 } catch (error) {
18 console.error('❌ Generation failed:', error);
19 }
20}
Conclusion
The AI SDK's structured output features make it incredibly easy to get reliable, type-safe responses from AI models. Whether you're building a simple text generator or a complex AI-powered application, these tools provide the foundation you need.
Key takeaways:
- Use
generateText
for simple text generation - Use
generateObject
with Zod schemas for structured data - Design intuitive, well-documented schemas
- Provide clear system prompts and examples
- Handle errors gracefully
- Test your generation thoroughly
With these techniques, you can build AI applications that are both powerful and reliable. The structured output ensures your AI responses integrate seamlessly with your existing codebase, making AI a first-class citizen in your applications.
Ready to implement structured output in your AI applications? Try ShipLog today and see how we use these techniques to generate perfect changelogs automatically.