Structured Output with Claude: JSON Mode and Beyond

When building applications with Claude, you often need structured data—not free-form text. JSON responses that you can parse, validate, and use programmatically. This guide shows you how to get reliable structured output from Claude every time.
The Structured Output Challenge
By default, Claude returns natural language. Ask it to "list the top 5 programming languages" and you might get:
- A numbered list
- A bullet-pointed list
- A paragraph with commas
- A JSON array (if you're lucky)
For applications, this inconsistency breaks things. You need predictable structure.
Method 1: Explicit JSON Instructions
The simplest approach is asking Claude directly for JSON:
const response = await anthropic.messages.create({
model: "claude-sonnet-4-20250514",
max_tokens: 1024,
messages: [{
role: "user",
content: `List the top 5 programming languages by popularity.
Respond with ONLY valid JSON in this format:
{
"languages": [
{"name": "string", "rank": number, "use_case": "string"}
]
}`
}]
});
This works most of the time. But Claude might:
- Add explanatory text before/after the JSON
- Use slightly different field names
- Include extra fields you didn't ask for
Method 2: System Prompt Enforcement
Move the JSON requirement to the system prompt for consistent behavior:
const response = await anthropic.messages.create({
model: "claude-sonnet-4-20250514",
max_tokens: 1024,
system: `You are a JSON API. You ONLY respond with valid JSON.
Never include explanations, markdown, or text outside the JSON.
If you cannot fulfill a request, respond with: {"error": "reason"}`,
messages: [{
role: "user",
content: `List top 5 programming languages:
{"languages": [{"name": "", "rank": 0, "use_case": ""}]}`
}]
});
Providing the exact schema in the user message acts as a template Claude will follow.
Method 3: JSON Mode (Recommended)
Anthropic's API supports a native JSON mode that guarantees valid JSON output:
const response = await anthropic.messages.create({
model: "claude-sonnet-4-20250514",
max_tokens: 1024,
messages: [{
role: "user",
content: "List the top 5 programming languages with their use cases"
}],
response_format: { type: "json_object" }
});
// response.content is guaranteed to be valid JSON
const data = JSON.parse(response.content[0].text);
With JSON mode enabled:
- Output is always valid JSON
- No markdown code blocks
- No explanatory text
- Parse errors become impossible
Defining Schemas with Zod
For TypeScript applications, define your expected schema with Zod and validate responses:
import { z } from 'zod';
const LanguageSchema = z.object({
languages: z.array(z.object({
name: z.string(),
rank: z.number().int().positive(),
use_case: z.string(),
trending: z.boolean().optional()
}))
});
type LanguageResponse = z.infer<typeof LanguageSchema>;
async function getLanguages(): Promise<LanguageResponse> {
const response = await anthropic.messages.create({
model: "claude-sonnet-4-20250514",
max_tokens: 1024,
system: `Respond with JSON matching this schema:
${JSON.stringify(zodToJsonSchema(LanguageSchema))}`,
messages: [{
role: "user",
content: "List top 5 programming languages"
}]
});
const data = JSON.parse(response.content[0].text);
return LanguageSchema.parse(data); // Throws if invalid
}
This gives you:
- Type safety in your code
- Runtime validation
- Clear error messages when schemas don't match
Handling Nested Structures
Complex nested data requires clear schema definitions:
const schema = {
company: {
name: "string",
founded: "number (year)",
employees: [
{
name: "string",
role: "string",
department: "string",
reports_to: "string or null"
}
],
products: [
{
name: "string",
price: "number",
features: ["string"]
}
]
}
};
const response = await anthropic.messages.create({
model: "claude-sonnet-4-20250514",
max_tokens: 2048,
system: `Return JSON matching this exact schema.
Use null for unknown values, never omit fields:
${JSON.stringify(schema, null, 2)}`,
messages: [{
role: "user",
content: "Generate sample data for a fictional tech startup"
}]
});
Extracting JSON from Mixed Output
If you're using an older approach or Claude includes text with the JSON, extract it:
function extractJSON(text) {
// Try parsing the whole thing first
try {
return JSON.parse(text);
} catch {}
// Look for JSON object
const objectMatch = text.match(/\{[\s\S]*\}/);
if (objectMatch) {
try {
return JSON.parse(objectMatch[0]);
} catch {}
}
// Look for JSON array
const arrayMatch = text.match(/\[[\s\S]*\]/);
if (arrayMatch) {
try {
return JSON.parse(arrayMatch[0]);
} catch {}
}
throw new Error('No valid JSON found in response');
}
This is a fallback—prefer JSON mode when available.
Handling Arrays and Lists
For list responses, be explicit about the array format:
// Bad: Ambiguous
"List 5 items"
// Good: Explicit array structure
"Return a JSON array of exactly 5 objects: [{\"item\": \"...\"}]"
// Better: Full example
`Return JSON in this exact format:
{
"items": [
{"name": "Example", "description": "Example description"},
...4 more items...
],
"total": 5
}`
Error Handling
Build robust error handling for production:
async function getStructuredResponse(prompt, schema, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const response = await anthropic.messages.create({
model: "claude-sonnet-4-20250514",
max_tokens: 1024,
response_format: { type: "json_object" },
messages: [{
role: "user",
content: `${prompt}\n\nRespond with JSON matching: ${JSON.stringify(schema)}`
}]
});
const data = JSON.parse(response.content[0].text);
// Validate against schema
if (validateSchema(data, schema)) {
return data;
}
// Schema mismatch - retry with more explicit instructions
console.warn(`Attempt ${attempt + 1}: Schema mismatch, retrying...`);
} catch (error) {
if (error.status === 429) {
await sleep(Math.pow(2, attempt) * 1000);
continue;
}
throw error;
}
}
throw new Error('Failed to get valid structured response after retries');
}
Streaming Structured Output
For long responses, stream the JSON and parse incrementally:
import { createParser } from 'streaming-json-parser';
async function streamStructuredResponse(prompt) {
const parser = createParser();
const stream = await anthropic.messages.create({
model: "claude-sonnet-4-20250514",
max_tokens: 4096,
stream: true,
response_format: { type: "json_object" },
messages: [{ role: "user", content: prompt }]
});
for await (const chunk of stream) {
if (chunk.type === 'content_block_delta') {
const partial = parser.write(chunk.delta.text);
if (partial) {
// Handle partial object as it's built
console.log('Partial data:', partial);
}
}
}
return parser.end();
}
Best Practices Checklist
For reliable structured output:
- Use JSON mode when your API version supports it
- Provide exact schemas with example values
- Include the schema in system prompts for consistency
- Validate responses with Zod or JSON Schema
- Handle edge cases — empty arrays, null values, missing fields
- Implement retries for transient failures
- Log parsing failures to improve prompts over time
Structured output transforms Claude from a conversational AI into a programmable API. Master these techniques and you can build reliable integrations that work every time.
More Articles
The Ultimate OpenClaw AWS Setup Guide

The definitive guide to setting up OpenClaw on AWS. Includes spot instance configuration, cost optimization, and step-by-step instructions.
Building AI Workflows with Tool Chaining in OpenClaw
Master the art of chaining tools and function calls to build powerful multi-step AI automation workflows—from data extraction to content generation and deployment.
Cost Optimization Guide for Self-Hosted AI Assistants: Run Claude on a Budget
Practical strategies to reduce API costs for self-hosted AI assistants—smart model routing, caching, batching, and OpenClaw-specific optimizations to run Claude affordably.