You ask Claude to return JSON, and it usually works. But sometimes it adds extra text, wraps it in markdown code blocks, or returns invalid JSON. Structured output fixes this — it guarantees valid JSON that matches your schema.
This is Article 10 in the Claude AI — From Zero to Power User series. You should have completed Article 7: Messages API before this article.
By the end of this article, you will know two ways to get reliable structured data from Claude, and when to use each one.
The Problem
Without structured output, this happens:
message = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
messages=[{"role": "user", "content": "Extract the product name and price from: 'The new MacBook Pro M4 costs $2,499'"}]
)
Claude might return:
Here's the extracted data:
```json
{"product": "MacBook Pro M4", "price": 2499}
The JSON is correct, but it is wrapped in text and markdown. Your json.loads() call will fail.
Structured output ensures Claude returns only valid JSON — no extra text, no markdown, no surprises.
---
## Approach 1: JSON Output Mode
The cleanest way to get structured data. You define a schema, and Claude returns JSON that matches it exactly.
### Python
```python
import anthropic
import json
client = anthropic.Anthropic()
message = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
messages=[
{
"role": "user",
"content": "Extract the product name and price from: 'The new MacBook Pro M4 costs $2,499'"
}
],
output_config={
"format": {
"type": "json_schema",
"json_schema": {
"name": "product_extraction",
"schema": {
"type": "object",
"properties": {
"product_name": {
"type": "string",
"description": "The product name"
},
"price": {
"type": "number",
"description": "The price in USD"
},
"currency": {
"type": "string",
"description": "The currency code"
}
},
"required": ["product_name", "price", "currency"],
"additionalProperties": False
},
"strict": True
}
}
}
)
# Safe to parse — always valid JSON matching the schema
result = json.loads(message.content[0].text)
print(result)
# {"product_name": "MacBook Pro M4", "price": 2499, "currency": "USD"}
TypeScript
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const message = await client.messages.create({
model: "claude-sonnet-4-6",
max_tokens: 1024,
messages: [
{
role: "user",
content:
"Extract the product name and price from: 'The new MacBook Pro M4 costs $2,499'",
},
],
output_config: {
format: {
type: "json_schema",
json_schema: {
name: "product_extraction",
schema: {
type: "object",
properties: {
product_name: {
type: "string",
description: "The product name",
},
price: {
type: "number",
description: "The price in USD",
},
currency: {
type: "string",
description: "The currency code",
},
},
required: ["product_name", "price", "currency"],
additionalProperties: false,
},
strict: true,
},
},
},
});
if (message.content[0].type === "text") {
const result = JSON.parse(message.content[0].text);
console.log(result);
}
With strict: true, Claude’s output is guaranteed to match the schema. No extra fields, no missing required fields, correct types.
Approach 2: Strict Tool Use
Instead of a JSON output mode, you can define a tool with a strict schema. This uses the tool use system from Article 8.
Python
import anthropic
import json
client = anthropic.Anthropic()
tools = [
{
"name": "extract_product",
"description": "Extract product information from text",
"input_schema": {
"type": "object",
"properties": {
"product_name": {"type": "string"},
"price": {"type": "number"},
"currency": {"type": "string"}
},
"required": ["product_name", "price", "currency"]
},
"strict": True
}
]
message = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
tools=tools,
tool_choice={"type": "tool", "name": "extract_product"},
messages=[
{"role": "user", "content": "The new MacBook Pro M4 costs $2,499"}
]
)
# The tool_use block contains the structured data
tool_block = next(b for b in message.content if b.type == "tool_use")
result = tool_block.input
print(result)
# {"product_name": "MacBook Pro M4", "price": 2499, "currency": "USD"}
TypeScript
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const tools: Anthropic.Tool[] = [
{
name: "extract_product",
description: "Extract product information from text",
input_schema: {
type: "object" as const,
properties: {
product_name: { type: "string" },
price: { type: "number" },
currency: { type: "string" },
},
required: ["product_name", "price", "currency"],
},
strict: true,
},
];
const message = await client.messages.create({
model: "claude-sonnet-4-6",
max_tokens: 1024,
tools,
tool_choice: { type: "tool", name: "extract_product" },
messages: [
{ role: "user", content: "The new MacBook Pro M4 costs $2,499" },
],
});
const toolBlock = message.content.find(
(b): b is Anthropic.ToolUseBlock => b.type === "tool_use"
);
if (toolBlock) {
console.log(toolBlock.input);
}
With tool_choice: {type: "tool", name: "extract_product"}, Claude always calls that tool. With strict: true, the input always matches the schema.
JSON Output Mode vs Strict Tool Use
| Feature | JSON Output Mode | Strict Tool Use |
|---|---|---|
| Where result lives | content[0].text (string) | content[N].input (object) |
| Needs parsing | Yes (json.loads) | No (already an object) |
| Can combine with text | No (pure JSON only) | Yes (text + tool_use blocks) |
| Multiple outputs | One JSON object | Multiple tool calls possible |
| Use case | Data extraction, API responses | Programmatic workflows, tool chains |
Use JSON output mode when you want pure structured data back — data extraction, classification, API responses.
Use strict tool use when structured output is part of a larger tool workflow — the agent calls your extraction tool, then uses the result in another tool.
Defining Schemas
Both approaches use JSON Schema. Here are common patterns.
Important: When using strict: true, every object in your schema must include "additionalProperties": false. Without this, the API returns an error. This applies to nested objects too.
Enums
{
"sentiment": {
"type": "string",
"enum": ["positive", "negative", "neutral"],
"description": "The overall sentiment"
}
}
Arrays
{
"tags": {
"type": "array",
"items": {"type": "string"},
"description": "List of relevant tags"
}
}
Nested Objects
{
"address": {
"type": "object",
"properties": {
"street": {"type": "string"},
"city": {"type": "string"},
"country": {"type": "string"},
"zip": {"type": "string"}
},
"required": ["street", "city", "country"]
}
}
Optional Fields
Fields not in the required array are optional. Claude may omit them or set them to null.
{
"type": "object",
"properties": {
"name": {"type": "string"},
"email": {"type": "string"},
"phone": {"type": "string"}
},
"required": ["name", "email"]
}
Here, phone is optional. Claude includes it when found in the input, omits it otherwise.
Practical Example: Support Ticket Classification
Python
import anthropic
import json
client = anthropic.Anthropic()
def classify_ticket(ticket_text: str) -> dict:
message = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
messages=[
{"role": "user", "content": f"Classify this support ticket:\n\n{ticket_text}"}
],
output_config={
"format": {
"type": "json_schema",
"json_schema": {
"name": "ticket_classification",
"schema": {
"type": "object",
"properties": {
"category": {
"type": "string",
"enum": ["bug", "feature_request", "billing", "account", "other"],
"description": "The ticket category"
},
"priority": {
"type": "string",
"enum": ["low", "medium", "high", "critical"],
"description": "How urgent is this ticket"
},
"summary": {
"type": "string",
"description": "One sentence summary of the issue"
},
"suggested_team": {
"type": "string",
"enum": ["engineering", "billing", "support", "product"],
"description": "Which team should handle this"
}
},
"required": ["category", "priority", "summary", "suggested_team"]
},
"strict": True
}
}
}
)
return json.loads(message.content[0].text)
# Use it
ticket = "I've been charged twice for my subscription this month. My account shows two payments of $49.99 on March 5 and March 7. Please refund the duplicate charge."
result = classify_ticket(ticket)
print(json.dumps(result, indent=2))
# {
# "category": "billing",
# "priority": "high",
# "summary": "Customer was charged twice for monthly subscription, requesting refund",
# "suggested_team": "billing"
# }
TypeScript
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
async function classifyTicket(ticketText: string) {
const message = await client.messages.create({
model: "claude-sonnet-4-6",
max_tokens: 1024,
messages: [
{ role: "user", content: `Classify this support ticket:\n\n${ticketText}` },
],
output_config: {
format: {
type: "json_schema",
json_schema: {
name: "ticket_classification",
schema: {
type: "object",
properties: {
category: {
type: "string",
enum: ["bug", "feature_request", "billing", "account", "other"],
},
priority: {
type: "string",
enum: ["low", "medium", "high", "critical"],
},
summary: { type: "string" },
suggested_team: {
type: "string",
enum: ["engineering", "billing", "support", "product"],
},
},
required: ["category", "priority", "summary", "suggested_team"],
},
strict: true,
},
},
},
});
if (message.content[0].type === "text") {
return JSON.parse(message.content[0].text);
}
}
const result = await classifyTicket(
"I've been charged twice for my subscription this month."
);
console.log(result);
Cost example: Classifying a support ticket with Sonnet 4.6 costs approximately $0.001-0.002 per ticket. At scale (10,000 tickets/month), that is about $10-20/month.
Validation with Pydantic (Python)
For production Python code, use Pydantic models to validate and type the response.
import anthropic
import json
from pydantic import BaseModel
from enum import Enum
class Category(str, Enum):
BUG = "bug"
FEATURE_REQUEST = "feature_request"
BILLING = "billing"
ACCOUNT = "account"
OTHER = "other"
class Priority(str, Enum):
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
CRITICAL = "critical"
class TicketClassification(BaseModel):
category: Category
priority: Priority
summary: str
suggested_team: str
client = anthropic.Anthropic()
message = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
messages=[{"role": "user", "content": "My app crashes when I open settings"}],
output_config={
"format": {
"type": "json_schema",
"json_schema": {
"name": "ticket_classification",
"schema": TicketClassification.model_json_schema(),
"strict": True
}
}
}
)
# Parse and validate with Pydantic
ticket = TicketClassification.model_validate_json(message.content[0].text)
print(ticket.category) # Category.BUG
print(ticket.priority) # Priority.HIGH
print(ticket.summary) # "App crashes when opening settings"
Pydantic gives you type safety, auto-completion in your IDE, and validation — all on top of Claude’s structured output.
Validation with Zod (TypeScript)
The TypeScript equivalent uses Zod for validation.
import Anthropic from "@anthropic-ai/sdk";
import { z } from "zod";
const client = new Anthropic();
const TicketSchema = z.object({
category: z.enum(["bug", "feature_request", "billing", "account", "other"]),
priority: z.enum(["low", "medium", "high", "critical"]),
summary: z.string(),
suggested_team: z.enum(["engineering", "billing", "support", "product"]),
});
type Ticket = z.infer<typeof TicketSchema>;
const message = await client.messages.create({
model: "claude-sonnet-4-6",
max_tokens: 1024,
messages: [
{ role: "user", content: "My app crashes when I open settings" },
],
output_config: {
format: {
type: "json_schema",
json_schema: {
name: "ticket_classification",
schema: {
type: "object",
properties: {
category: {
type: "string",
enum: ["bug", "feature_request", "billing", "account", "other"],
},
priority: {
type: "string",
enum: ["low", "medium", "high", "critical"],
},
summary: { type: "string" },
suggested_team: {
type: "string",
enum: ["engineering", "billing", "support", "product"],
},
},
required: ["category", "priority", "summary", "suggested_team"],
},
strict: true,
},
},
},
});
if (message.content[0].type === "text") {
const ticket: Ticket = TicketSchema.parse(
JSON.parse(message.content[0].text)
);
console.log(ticket.category); // "bug"
console.log(ticket.priority); // "high"
}
Structured Output with Streaming
You can combine structured output with streaming. The JSON arrives in chunks.
Python
import anthropic
import json
client = anthropic.Anthropic()
collected = ""
with client.messages.stream(
model="claude-sonnet-4-6",
max_tokens=2048,
messages=[
{"role": "user", "content": "Analyze the top 5 programming languages for 2026"}
],
output_config={
"format": {
"type": "json_schema",
"json_schema": {
"name": "language_analysis",
"schema": {
"type": "object",
"properties": {
"languages": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {"type": "string"},
"rank": {"type": "integer"},
"trend": {"type": "string", "enum": ["rising", "stable", "declining"]},
"reason": {"type": "string"}
},
"required": ["name", "rank", "trend", "reason"]
}
}
},
"required": ["languages"]
},
"strict": True
}
}
}
) as stream:
for text in stream.text_stream:
collected += text
print(text, end="", flush=True)
print()
result = json.loads(collected)
print(f"\nTop language: {result['languages'][0]['name']}")
The JSON arrives incrementally. You can parse partial JSON to show progress, or wait until the stream completes for the full object.
Performance Notes
Structured output does not reduce model quality. Claude is just as smart when outputting JSON — it is simply constrained to produce valid JSON matching your schema.
The schema itself adds tokens to the input (typically 100-300 tokens), which is a small cost increase. But you save by never getting invalid responses that require retries.
Summary
| Approach | Best For | Result Location |
|---|---|---|
| JSON output mode | Pure data extraction | content[0].text (JSON string) |
| Strict tool use | Tool chains, workflows | tool_use.input (object) |
| Pydantic (Python) | Type-safe validation | Pydantic model instance |
| Zod (TypeScript) | Type-safe validation | Zod-validated object |
Structured output eliminates JSON parsing errors. Use it whenever you need machine-readable data from Claude.
What’s Next?
In the next article, we will cover Prompt Caching — a powerful feature that can cut your Claude API costs by up to 90%.
Next: Prompt Caching — Save Money on Repeated Context