Request JSON Output
Why Ask for JSON?
By default, Claude replies in natural language, which is ideal for people and awkward for programs. When your code needs to use the result, by storing it in a database, returning it from an API, filling a form, or making a decision, you need structured data. JSON (JavaScript Object Notation) is the most common choice, because Python reads it directly into dictionaries and lists:
import json
data = json.loads('{"category": "billing", "urgent": true}')
print(data["category"]) # billing
print(data["urgent"]) # True
There are two ways to get JSON from Claude:
| Approach | How | Guarantee |
|---|---|---|
| Prompt-based JSON | Ask for JSON in the prompt, then parse the text | Usually works; not guaranteed |
| Structured outputs | Send a JSON schema in the request (output_config.format) | Valid JSON matching the schema |
This lesson covers both: prompt-based JSON, because you will meet it everywhere and should know its limits, and structured outputs, which is the recommended approach for production. Lessons 101–110 explore structured outputs in depth.
Approach 1: Prompt-Based JSON
Ask Clearly and Show the Shape
from anthropic import Anthropic
client = Anthropic()
prompt = """Extract the event details from the text below.
<text>
Join our Django workshop on 15 November 2026 at 6 PM IST. It's free, register at gocourse.example/django.
</text>
Return only a JSON object with exactly these keys, and no other text:
{"title": string, "date": "YYYY-MM-DD", "time": "HH:MM", "free": boolean, "url": string}"""
reply = client.messages.create(
model="claude-opus-5-5",
max_tokens=1024,
messages=[{"role": "user", "content": prompt}],
)
text = "".join(block.text for block in reply.content if block.type == "text").strip()
print(text)
Typical output:
{"title": "Django workshop", "date": "2026-11-15", "time": "18:00", "free": true, "url": "gocourse.example/django"}
Why the prompt is written this way:
- "Return only a JSON object… and no other text" discourages explanations before or after the JSON.
- The shape with types shows exact key names, types, and formats (
YYYY-MM-DD, 24-hourHH:MM), so Claude does not have to guess. - The data is in tags, separate from the instructions (Lesson 18).
What Can Go Wrong
Prompt-based JSON works most of the time, but your code must be ready for these cases:
| Problem | Example | Cause |
|---|---|---|
| Extra text | Here is the JSON: {...} | The model added a preface |
| Code fences | ```json\n{...}\n``` | The model formatted the JSON as a Markdown code block |
| Invalid JSON | Trailing commas, single quotes | Generation is not constrained to JSON syntax |
| Wrong keys or types | "is_free": "yes" | Instructions not followed exactly |
| Truncation | {"title": "Django wor | max_tokens reached (Lesson 13) |
Note: Older tutorials end the message list with an assistant message containing
{(prefill) to force JSON. Current models (Claude 4.6 and later) do not support prefill, and such requests return a400error (Lesson 18). Use structured outputs instead.
Parsing Defensively
A small helper handles the common formatting problems:
import json
import re
def extract_json(text: str) -> dict:
"""Parse a JSON object from model text, tolerating code fences and surrounding prose."""
cleaned = text.strip()
fenced = re.search(r"```(?:json)?\s*(.*?)\s*```", cleaned, re.DOTALL)
if fenced:
cleaned = fenced.group(1)
start, end = cleaned.find("{"), cleaned.rfind("}")
if start == -1 or end == -1:
raise ValueError("No JSON object found in the response.")
return json.loads(cleaned[start:end + 1])
How it works:
- The regular expression looks for a fenced code block (optionally labeled
json) and keeps only its contents.re.DOTALLlets.match line breaks. find("{")andrfind("}")locate the first opening brace and the last closing brace, which cuts away any prose before or after the object.json.loads()parses the result and raisesjson.JSONDecodeErrorif the JSON itself is invalid.
Validating the Data
Parsing proves the text is JSON, not that it has the right content. Check required keys and types:
REQUIRED = {"title": str, "date": str, "time": str, "free": bool, "url": str}
def validate(data: dict) -> list[str]:
problems = []
for key, expected_type in REQUIRED.items():
if key not in data:
problems.append(f"missing key: {key}")
elif not isinstance(data[key], expected_type):
problems.append(f"{key} should be {expected_type.__name__}")
return problems
Retrying with Feedback
If parsing or validation fails, ask Claude to correct its output. Send the faulty reply back with the specific error, so the correction is targeted:
messages = [
{"role": "user", "content": prompt},
{"role": "assistant", "content": text},
{"role": "user", "content": f"That JSON was invalid: {error}. Return only the corrected JSON object."},
]
This works, but it costs an extra request. Structured outputs avoid the problem entirely.
Approach 2: Structured Outputs with a JSON Schema
Structured outputs constrain Claude's response to follow a JSON Schema that you provide. According to the documentation, the response is then valid JSON that matches your schema. There is no parsing guesswork, no missing required fields, and no wrong types.
You send the schema in output_config.format:
import json
from anthropic import Anthropic
client = Anthropic()
EVENT_SCHEMA = {
"type": "object",
"properties": {
"title": {"type": "string"},
"date": {"type": "string", "description": "Date in YYYY-MM-DD format"},
"time": {"type": "string", "description": "24-hour time, HH:MM"},
"free": {"type": "boolean"},
"url": {"type": "string"},
},
"required": ["title", "date", "time", "free", "url"],
"additionalProperties": False,
}
reply = client.messages.create(
model="claude-opus-5-5",
max_tokens=1024,
messages=[{
"role": "user",
"content": "Extract the event details.\n<text>\nJoin our Django workshop on 15 November 2026 "
"at 6 PM IST. It's free, register at gocourse.example/django.\n</text>",
}],
output_config={"format": {"type": "json_schema", "schema": EVENT_SCHEMA}},
)
text = "".join(block.text for block in reply.content if block.type == "text")
event = json.loads(text)
print(event["date"], event["free"]) # 2026-11-15 True
How the schema works:
| Schema part | Meaning |
|---|---|
"type": "object" | The result is a JSON object |
"properties" | The allowed keys and the type of each value |
"description" | Guidance on the expected content and format of a field |
"required" | Keys that must always be present |
"additionalProperties": False | No keys other than those listed |
The JSON arrives in the response's text block, so it is extracted in the usual way: by filtering blocks by type, since a thinking block may come first.
Note: Structured outputs support most of JSON Schema, but the documentation lists some limitations. Check the "Structured outputs" page when you use advanced schema features.
Still Check the Stop Reason
A schema controls the format of a normal, complete response. It cannot complete a response that was cut off by max_tokens, or replace a refusal. Check stop_reason before parsing:
if reply.stop_reason != "end_turn":
raise RuntimeError(f"Incomplete response: {reply.stop_reason}")
Approach 3: Pydantic Models with messages.parse()
The Python SDK can build the schema from a Pydantic model and return a typed object. According to the documentation, this is the recommended method in Python: pass the model to client.messages.parse() as output_format, and read the result from parsed_output.
from anthropic import Anthropic
from pydantic import BaseModel
class Event(BaseModel):
title: str
date: str
time: str
free: bool
url: str
client = Anthropic()
response = client.messages.parse(
model="claude-opus-5-5",
max_tokens=1024,
output_format=Event,
messages=[{
"role": "user",
"content": "Extract the event details.\n<text>\nJoin our Django workshop on 15 November 2026 "
"at 6 PM IST. It's free, register at gocourse.example/django.\n</text>",
}],
)
event = response.parsed_output
print(event.title, event.date, event.free) # Django workshop 2026-11-15 True
What the SDK does for you:
- Converts the Pydantic model into a JSON schema.
- Sends it as
output_config.format. - Validates the response against the model.
- Returns an
Eventinstance inparsed_output, so you get attribute access, type hints, and editor autocomplete.
Pydantic is already installed as a dependency of the SDK. Lessons 104–110 cover Pydantic models in detail, including nested objects, lists, and validation.
Choosing an Approach
| Prompt-based JSON | Structured outputs (schema) | messages.parse() + Pydantic | |
|---|---|---|---|
| Valid JSON guaranteed | No | Yes | Yes |
| Schema enforced | No | Yes | Yes |
| Parsing code needed | Yes, defensive | json.loads | None |
| Result type | dict | dict | Typed model instance |
| Best for | Quick experiments, models or platforms without structured outputs | Schemas from files or other systems | Python applications |
Recommendation: use messages.parse() with Pydantic in Python applications, raw JSON schemas when the schema comes from elsewhere, and prompt-based JSON only for quick experiments or where structured outputs are not available.
Practical Example: A Contact Extractor
This example extracts contact details from free text using a Pydantic model, checks the stop reason, and handles missing information explicitly.
"""extract_contacts.py: extract structured contact details from free text."""
from anthropic import Anthropic
from pydantic import BaseModel
class Contact(BaseModel):
name: str
email: str
phone: str
company: str
client = Anthropic()
SYSTEM = (
"You extract contact details from text. "
"Use an empty string for any field that is not present in the text. Never invent values."
)
TEXTS = [
"Hi, this is Priya Raman from Northwind Labs. Reach me at priya@northwind.example or +91 98765 43210.",
"Thanks for the call! — Arjun (arjun.k@example.com)",
"Please send the invoice to our accounts team.",
]
def extract(text: str) -> Contact | None:
response = client.messages.parse(
model="claude-opus-5-5",
max_tokens=1024,
system=SYSTEM,
output_format=Contact,
messages=[{"role": "user", "content": f"<text>\n{text}\n</text>"}],
)
if response.stop_reason != "end_turn":
print(f" [skipped: stop_reason={response.stop_reason}]")
return None
return response.parsed_output
if __name__ == "__main__":
for text in TEXTS:
contact = extract(text)
if contact is None:
continue
found = {field: value for field, value in contact.model_dump().items() if value}
print(f"{text[:45]!r:<50} -> {found or 'no contact details'}")
Example output:
'Hi, this is Priya Raman from Northwind Labs. ' -> {'name': 'Priya Raman', 'email': 'priya@northwind.example', 'phone': '+91 98765 43210', 'company': 'Northwind Labs'}
'Thanks for the call! — Arjun (arjun.k@example' -> {'name': 'Arjun', 'email': 'arjun.k@example.com'}
'Please send the invoice to our accounts team.' -> no contact details
How It Works
Contactdefines the exact structure: four string fields. Its schema is sent automatically bymessages.parse().- The system prompt defines how to represent missing data (empty strings) and forbids inventing values. A schema guarantees structure, but the prompt still controls content.
stop_reasonis checked before usingparsed_output, because a truncated or refused response cannot be trusted.contact.model_dump()converts the model into a dictionary, and the comprehension keeps only fields with values, giving a compact display.- The third text shows correct handling of absent information: every field is empty, and nothing is fabricated.
Common Mistakes
Calling json.loads() on the Raw Text Without Checks
What happens: crashes on code fences, prose, or truncated output.
Fix: use structured outputs, or parse defensively and validate the result.
Using Assistant Prefill to Force JSON
What happens: a 400 error on current models.
Fix: use structured outputs.
Assuming a Schema Guarantees Correct Content
What happens: structurally valid JSON contains invented or wrong values.
Fix: instruct Claude how to handle missing data, ground extraction in the provided text, and validate important values in code.
Ignoring the Stop Reason
What happens: a truncated or refused response is parsed, causing errors or bad data.
Fix: check stop_reason before parsing.
Reading content[0].text for the JSON
What happens: AttributeError when a thinking block comes first.
Fix: collect the text blocks by type, or use parsed_output.
Best Practices
- Prefer structured outputs for anything your program depends on.
- Use
messages.parse()with Pydantic models in Python. - Describe fields clearly, with formats, in the schema or the model.
- Tell Claude how to represent missing information, and forbid invented values.
- Check
stop_reasonbefore using parsed data. - Keep defensive parsing for prompt-based JSON, and retry with specific error feedback when needed.
- Validate business rules in code, such as date ranges and allowed values, even with a schema.
Exercise
- Use prompt-based JSON to extract a book's title, author, and year from a sentence. Parse it with
extract_json()and validate it. - Rewrite step 1 using a raw JSON schema in
output_config.format. - Rewrite step 1 again using a Pydantic model and
messages.parse(). Which version needs the least code? - Add a field
"role": str(job title) toContact, and test it on the three sample texts. How is the missing role represented?
from anthropic import Anthropic
from pydantic import BaseModel
class Book(BaseModel):
title: str
author: str
year: int
client = Anthropic()
response = client.messages.parse(
model="claude-opus-5-5",
max_tokens=1024,
output_format=Book,
messages=[{
"role": "user",
"content": "<text>\nOrwell's novel Nineteen Eighty-Four was published in 1949.\n</text>",
}],
)
if response.stop_reason == "end_turn":
book = response.parsed_output
print(book.title, book.author, book.year)
This version needs no parsing or validation code. The SDK builds the schema from Book, Claude's output must match it, and year arrives as an int. For step 4, the system prompt's rule produces an empty string for role when no job title is present.