TypeScript AI Structured Outputs
A practical reference for generating guaranteed type-safe JSON data from AI models using Zod schemas and structured output modes.
TL;DR
- Define expected data structures using
z.object()and Zod primitives. - Derive static TypeScript types directly from runtime schemas with
z.infer<typeof Schema>. - Enforce JSON output schemas with native provider
response_formatflags.
Schema Definition with Zod
z.object() definitionDeclare structured payload shape using Zod schema builders.
import { z } from 'zod';
const UserSchema = z.object({
name: z.string().describe('Full user name'),
age: z.number().int().positive(),
roles: z.array(z.enum(['admin', 'editor'])),
});z.infer<typeof Schema>Derive static TypeScript types directly from runtime schemas.
type User = z.infer<typeof UserSchema>;
// Inferred type:
// {
// name: string;
// age: number;
// roles: ('admin' | 'editor')[];
// }Optional and nullable fieldsModel optional properties and nullable attributes cleanly.
const PostSchema = z.object({
title: z.string(),
summary: z.string().optional(),
publishedAt: z.string().datetime().nullable(),
});Native Provider Configuration
OpenAI json_schema modeEnforce strict JSON schema compliance with OpenAI models.
import { zodResponseFormat } from 'openai/helpers/zod';
const res = await openai.chat.completions.create({
model: 'gpt-4o',
messages: [{ role: 'user', content: 'Extract' }],
response_format: zodResponseFormat(UserSchema, 'u'),
});Gemini responseSchemaConfigure schema constraints in Google GenAI client.
const res = await ai.models.generateContent({
model: 'gemini-2.0-flash',
contents: 'Extract user info',
config: {
responseMimeType: 'application/json',
responseSchema: { type: 'OBJECT' },
},
});Anthropic tool constraintSimulate structured outputs in Claude using tool choice.
const res = await anthropic.messages.create({
model: 'claude-3-5-sonnet-20241022',
max_tokens: 500,
tools: [{
name: 'save_user',
description: 'Save user',
input_schema: { type: 'object' },
}],
tool_choice: { type: 'tool', name: 'save_user' },
messages: [{ role: 'user', content: 'Extract' }],
});Safe Parsing and Type Narrowing
Schema.safeParse()Validate raw JSON string safely without throwing exceptions.
const parsed = UserSchema.safeParse(JSON.parse(raw));
if (!parsed.success) {
console.error(parsed.error.issues);
} else {
const user: User = parsed.data;
console.log('User:', user.name);
}Format Zod errorsExtract readable validation messages for user feedback.
function formatErrors(err: z.ZodError): string {
return err.issues
.map(i => `${i.path.join('.')}: ${i.message}`)
.join(', ');
}Type-safe wrapper functionCreate reusable generic helper to extract typed objects.
async function extractObject<T>(
schema: z.ZodType<T>, prompt: string
): Promise<T> {
const raw = await fetchCompletion(prompt);
return schema.parse(JSON.parse(raw));
}Automated Schema Self-Repair
Self-repair prompt loopRe-prompt model with validation error details on failure.
async function repairJson<T>(
schema: z.ZodType<T>, bad: string, err: string
): Promise<T> {
const res = await openai.chat.completions.create({
model: 'gpt-4o',
messages: [
{ role: 'user', content: `${bad}: ${err}` },
],
});
const txt = res.choices[0].message.content!;
return schema.parse(JSON.parse(txt));
}Retry with backoff loopAttempt parsing up to three times before throwing error.
for (let i = 0; i < 3; i++) {
try {
return await extractObject(UserSchema, prompt);
} catch {}
}
throw new Error('Retries failed');Schema default valuesProvide fallback defaults for non-critical attributes.
const SettingsSchema = z.object({
theme: z.enum(['light', 'dark']).default('light'),
retries: z.number().default(3),
});Tips
- Use
z.infer<typeof Schema>to keep your compile-time TypeScript interfaces perfectly synchronized with your runtime model validation rules. - Attach
.describe()helper annotations to your Zod schema keys to provide contextual instructions that guide the model during extraction.
Warnings
- Never assume an AI model response is valid JSON without wrapping
JSON.parseand.safeParse()inside defensive error handlers. - Avoid overly deep nested object schemas because recursive validation constraints can cause models to exhaust their
max_tokensbudget.
In Practice
An AI extraction pipeline that parses unstructured text into verified TypeScript types using Zod schemas.
- Declare the target entity shape using a strict Zod schema.
- Request a completion using OpenAI native zodResponseFormat.
- Perform runtime validation with safeParse to guarantee data integrity.
- Access typed properties safely without type assertions or casts.
import { OpenAI } from 'openai';
import { z } from 'zod';
import { zodResponseFormat } from 'openai/helpers/zod';
const ProductSchema = z.object({
id: z.string(),
name: z.string(),
price: z.number().positive(),
inStock: z.boolean(),
});
export async function parseProduct(text: string) {
const openai = new OpenAI();
const res = await openai.chat.completions.create({
model: 'gpt-4o',
messages: [{ role: 'user', content: text }],
response_format: zodResponseFormat(ProductSchema, 'product'),
});
const raw = res.choices[0].message.content || '{}';
return ProductSchema.parse(JSON.parse(raw));
}FAQ
Regular json_object mode only guarantees that the output parses as valid JSON, but keys and types may wander. The native json_schema mode uses constrained grammar decoding to guarantee that the output matches your exact schema.
Use helper libraries like zod-to-json-schema to convert your z.object() into a valid JSON Schema object. Pass the resulting schema inside the response_format: { type: 'json_schema', json_schema: {...} } property.
When safeParse() fails, capture the formatted error messages from result.error.issues. Re-prompt the model with the invalid output and error list to perform an automated self-repair loop.