Structured Output from AI Models Explained (2026)
LLMs generate text. Your app needs JSON. Structured output bridges the gap — reliable, typed data from AI models every time. No more parsing markdown, no more regex on GPT output. Here's how it works.
The Problem
// ❌ Without structured output
const response = await llm.chat("Extract the name and email from this text: ...")
// Returns: "The name is John Smith and the email is john@example.com"
// Now you need regex or string parsing to extract data 😩
// What if it returns "Name: John Smith, Email: john@example.com" instead?
// Or "John Smith (john@example.com)"?
// ✅ With structured output
const response = await llm.chat("Extract the name and email", {
response_format: { name: "string", email: "string" }
})
// Returns: { name: "John Smith", email: "john@example.com" }
// Every time. Guaranteed JSON. Typed. 🎉
How It Works
Structured output constrains the LLM's generation:
Normal generation:
Token 1: "The" | "Here" | "{" | "Name" → any token possible
Structured output (JSON mode):
Token 1: must be "{" (JSON object start)
Token 2: must be "\"" (property name start)
Token 3: must match schema field name
...continues following JSON grammar rules
The model can only generate tokens that produce valid JSON matching your schema. This is enforced at the token level, not by asking nicely in the prompt.
OpenAI Structured Output
JSON Mode (Simple)
const response = await openai.chat.completions.create({
model: 'gpt-4o',
response_format: { type: 'json_object' },
messages: [
{ role: 'system', content: 'Return a JSON object with name and email fields.' },
{ role: 'user', content: 'Extract from: Contact John at john@acme.com' },
],
})
const data = JSON.parse(response.choices[0].message.content)
// { name: "John", email: "john@acme.com" }
Strict Schema (Better)
const response = await openai.chat.completions.create({
model: 'gpt-4o',
response_format: {
type: 'json_schema',
json_schema: {
name: 'contact_extraction',
strict: true,
schema: {
type: 'object',
properties: {
name: { type: 'string', description: 'Full name' },
email: { type: 'string', description: 'Email address' },
company: { type: 'string', description: 'Company name' },
role: {
type: 'string',
enum: ['engineer', 'manager', 'executive', 'other'],
},
},
required: ['name', 'email', 'company', 'role'],
additionalProperties: false,
},
},
},
messages: [
{ role: 'user', content: 'John Smith, CTO at Acme Corp (john@acme.com)' },
],
})
// Guaranteed to match your schema exactly
Anthropic Claude Structured Output
Using Tool Use (Recommended)
Claude's tool use feature is the best way to get structured output:
const response = await anthropic.messages.create({
model: 'claude-sonnet-4-20250514',
max_tokens: 1024,
tools: [{
name: 'extract_contact',
description: 'Extract contact information from text',
input_schema: {
type: 'object',
properties: {
name: { type: 'string', description: 'Full name' },
email: { type: 'string', description: 'Email address' },
company: { type: 'string', description: 'Company name' },
role: {
type: 'string',
enum: ['engineer', 'manager', 'executive', 'other'],
},
},
required: ['name', 'email', 'company', 'role'],
},
}],
tool_choice: { type: 'tool', name: 'extract_contact' }, // Force this tool
messages: [
{ role: 'user', content: 'John Smith, CTO at Acme Corp (john@acme.com)' },
],
})
// Extract structured data from tool use response
const toolUse = response.content.find(b => b.type === 'tool_use')
const data = toolUse.input
// { name: "John Smith", email: "john@acme.com", company: "Acme Corp", role: "executive" }
With Vercel AI SDK (Recommended for TypeScript)
The Vercel AI SDK makes structured output seamless:
import { generateObject } from 'ai'
import { openai } from '@ai-sdk/openai'
import { z } from 'zod'
// Define schema with Zod — get TypeScript types automatically
const contactSchema = z.object({
name: z.string().describe('Full name'),
email: z.string().email().describe('Email address'),
company: z.string().describe('Company name'),
role: z.enum(['engineer', 'manager', 'executive', 'other']),
sentiment: z.enum(['positive', 'neutral', 'negative'])
.describe('Tone of the communication'),
})
const { object } = await generateObject({
model: openai('gpt-4o'),
schema: contactSchema,
prompt: 'John Smith, CTO at Acme Corp (john@acme.com), very interested in our product',
})
// object is fully typed as:
// { name: string, email: string, company: string, role: "engineer"|"manager"|..., sentiment: "positive"|"neutral"|"negative" }
console.log(object.name) // "John Smith" — TypeScript knows this is a string
console.log(object.sentiment) // "positive" — TypeScript knows the enum values
Common Patterns
Pattern 1: Data Extraction
const invoiceSchema = z.object({
vendor: z.string(),
invoiceNumber: z.string(),
date: z.string().describe('ISO 8601 date'),
lineItems: z.array(z.object({
description: z.string(),
quantity: z.number(),
unitPrice: z.number(),
total: z.number(),
})),
subtotal: z.number(),
tax: z.number(),
total: z.number(),
})
const { object: invoice } = await generateObject({
model: openai('gpt-4o'),
schema: invoiceSchema,
prompt: `Extract invoice data from this text: ${invoiceText}`,
})
Pattern 2: Classification
const classificationSchema = z.object({
category: z.enum(['bug', 'feature', 'question', 'documentation']),
priority: z.enum(['low', 'medium', 'high', 'critical']),
summary: z.string().max(100),
suggestedAssignee: z.enum(['frontend', 'backend', 'devops', 'design']),
})
const { object } = await generateObject({
model: openai('gpt-4o-mini'), // Cheaper model works fine for classification
schema: classificationSchema,
prompt: `Classify this support ticket: "${ticketText}"`,
})
Pattern 3: Content Generation
const blogPostSchema = z.object({
title: z.string(),
slug: z.string().describe('URL-friendly slug'),
metaDescription: z.string().max(160),
tags: z.array(z.string()).max(5),
sections: z.array(z.object({
heading: z.string(),
content: z.string(),
})).min(3).max(8),
})
Pattern 4: Evaluation / Scoring
const evaluationSchema = z.object({
score: z.number().min(1).max(10),
strengths: z.array(z.string()).min(1).max(5),
weaknesses: z.array(z.string()).min(1).max(5),
suggestion: z.string(),
passesThreshold: z.boolean(),
})
Tips for Reliable Output
1. Use Descriptions
// ❌ Bad — LLM has to guess what you want
z.object({ s: z.string(), n: z.number() })
// ✅ Good — clear descriptions guide the LLM
z.object({
sentiment: z.enum(['positive', 'negative', 'neutral'])
.describe('Overall emotional tone of the text'),
confidenceScore: z.number().min(0).max(1)
.describe('How confident the classification is, 0-1'),
})
2. Use Enums for Categorical Data
// ❌ Bad — LLM might return "High", "high", "HIGH", "3", "critical"
priority: z.string()
// ✅ Good — constrained to valid values
priority: z.enum(['low', 'medium', 'high', 'critical'])
3. Use Smaller Models When Possible
Structured output with simple schemas works great on cheaper models:
Classification tasks: gpt-4o-mini ($0.15/1M tokens)
Complex extraction: gpt-4o ($2.50/1M tokens)
Reasoning + structure: claude-sonnet ($3/1M tokens)
FAQ
Does structured output affect quality?
Minimally. The constraint forces valid JSON but the model still reasons about content normally. For complex reasoning, let the model think in a reasoning field before the answer fields.
What if my schema is very complex?
Break it into smaller schemas. Extract entities first, then classify, then relate. Chaining simple structured calls beats one complex call.
Can I use structured output with streaming?
Yes. The Vercel AI SDK supports streamObject() which streams partial JSON as it's generated. Great for showing progressive results in UIs.
What about open-source models?
Llama 3.1+, Mistral, and other models support structured output via constrained decoding. Libraries like Outlines (Python) or llama.cpp enforce JSON schemas at generation time.
Bottom Line
Structured output turns LLMs from text generators into data processors. Use Zod schemas + Vercel AI SDK for the best TypeScript experience. Use OpenAI's json_schema mode for direct API calls. Use Claude's tool_use for Anthropic models.
Stop parsing LLM text with regex. Define a schema, get typed data, build reliable applications.