Structured Outputs
Enforce strict type safety and JSON Schema compliance on AI model outputs using Zod validation and structured decoding.
TL;DR
- Define strict response structures using standard
zodobject schemas. - Enable guaranteed grammar compliance with
response_formatstrict mode. - Validate generated output strings with
safeParseto guarantee type safety.
Zod Schema Definition
Declare Target SchemaDefine type-safe entity schema with strict field descriptions.
import { z } from 'zod';
const UserProfileSchema = z.object({
name: z.string().describe('Full name'),
age: z.number().int().positive(),
skills: z.array(z.string()),
});
type UserProfile = z.infer<typeof UserProfileSchema>;Enforce Strict ObjectPrevent models from generating extra unverified object keys.
const StrictSchema = z.object({
status: z.enum(['active', 'pending']),
score: z.number().min(0).max(100),
}).strict();Nested Complex SchemasCompose multi-level hierarchical structures for complex data.
const ProjectSchema = z.object({
title: z.string(),
tasks: z.array(z.object({
id: z.string(),
done: z.boolean(),
})),
});OpenAI Structured Outputs
zodResponseFormat HelperPass Zod schemas directly into chat completions request format.
import { zodResponseFormat } from 'openai/helpers/zod';
const fmt = zodResponseFormat(
UserProfileSchema,
'profile'
);
const res = await openai.beta.chat.completions.parse({
model: 'gpt-4o-2024-08-06',
messages: [{ role: 'user', content: 'John, 30' }],
response_format: fmt,
});Direct Parsed AccessRead pre-validated typed object directly from completion message.
const p: UserProfile = res.choices[0].message.parsed!;
console.log(p.name, p.age);Refusal HandlingCheck if safety policies prevented structured generation.
if (res.choices[0].message.refusal) {
console.warn('Safety policy prevented generation');
}JSON Schema Native Mode
Raw JSON SchemaDeclare raw schema parameters with strict enforcement enabled.
const format = {
type: 'json_schema' as const,
json_schema: {
name: 'user_data',
strict: true,
schema: { type: 'object', properties: {} }
}
};Anthropic Tool ExtractionUse forced tool calls to extract structured data in Claude.
const tool = {
name: 'submit',
input_schema: rawSchema
};
const res = await anthropic.messages.create({
model: 'claude-3-5-sonnet-20241022',
max_tokens: 500,
tools: [tool],
tool_choice: { type: 'tool', name: 'submit' },
messages: [{ role: 'user', content: text }],
});Extract Tool JSONExtract parsed tool input argument object from Claude response.
const t = res.content.find(b => b.type === 'tool_use');
const data = (t as any)?.input;Validation And Fallbacks
SafeParse ValidationValidate raw generated JSON strings defensively with Zod.
const parsed = JSON.parse(rawJson);
const result = UserProfileSchema.safeParse(parsed);
if (!result.success) {
console.error(result.error.format());
}Markdown Fence StrippingClean unwanted markdown delimiters before JSON decoding.
// Strip backtick fences without literal triggers
function cleanJson(raw: string): string {
const fence = String.fromCharCode(96).repeat(3);
return raw.replaceAll(fence, '').trim();
}Schema Retry PromptRe-prompt model with specific validation errors for self-correction.
const errMsg = result.error.message;
const fixPrompt = `Fix JSON errors: ${errMsg}`;
const fixed = await model.complete(fixPrompt);Tips
- Set
strict: trueon JSON Schema definitions so models reject unspecified properties and follow exact types. - Add descriptive
describe()comments inside schema definitions to guide model field population accurately without bloating system prompts.
Warnings
- Do not rely solely on simple prompt instructions like return
JSON; models frequently wrap outputs in markdown code fences. - Avoid optional properties without explicit default fallbacks in your
zodschema to prevent unexpected downstream runtime exceptions.
In Practice
Extracts strongly-typed lead information from unstructured customer text using Zod and OpenAI structured outputs.
- Define strict customer lead schema with contact information and score.
- Call beta completions parse method with zodResponseFormat helper.
- Access validated typed data directly without manual JSON.parse.
- Process strongly-typed lead object inside application business logic.
import OpenAI from 'openai';
import { zodResponseFormat } from 'openai/helpers/zod';
import { z } from 'zod';
const client = new OpenAI();
const Lead = z.object({
company: z.string(),
budget: z.number().int().positive(),
urgency: z.enum(['low', 'medium', 'high']),
});
async function getLead(text: string) {
const fmt = zodResponseFormat(Lead, 'lead');
const res = await client.chat.completions.create({
model: 'gpt-4o-mini',
messages: [{ role: 'user', content: text }],
response_format: fmt,
});
return res.choices[0]?.message?.content;
}
console.log(await getLead('Acme budget $50k fast'));FAQ
JSON Mode only guarantees that the model emits syntactically valid JSON, but keys and types can still deviate from requirements. Structured Outputs uses constrained grammar decoding to guarantee that emitted tokens follow your exact JSON Schema 100% of the time.
During sampling, the inference engine masks out any tokens that would violate the provided JSON Schema grammar. This makes it mathematically impossible for the model to produce invalid fields or missing required keys.
Use the zodResponseFormat helper from the OpenAI SDK. Pass your Zod schema into response_format: zodResponseFormat(Schema, 'name'), and the SDK converts it to JSON Schema automatically.