Request JSON Output

Su Suriya Ravichandran Updated 10 Nov 2026
10 min read ·Lesson 36 of 200

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:

ApproachHowGuarantee
Prompt-based JSONAsk for JSON in the prompt, then parse the textUsually works; not guaranteed
Structured outputsSend 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-hour HH: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:

ProblemExampleCause
Extra textHere is the JSON: {...}The model added a preface
Code fences```json\n{...}\n```The model formatted the JSON as a Markdown code block
Invalid JSONTrailing commas, single quotesGeneration is not constrained to JSON syntax
Wrong keys or types"is_free": "yes"Instructions not followed exactly
Truncation{"title": "Django wormax_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 a 400 error (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:

  1. The regular expression looks for a fenced code block (optionally labeled json) and keeps only its contents. re.DOTALL lets . match line breaks.
  2. find("{") and rfind("}") locate the first opening brace and the last closing brace, which cuts away any prose before or after the object.
  3. json.loads() parses the result and raises json.JSONDecodeError if 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 partMeaning
"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": FalseNo 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:

  1. Converts the Pydantic model into a JSON schema.
  2. Sends it as output_config.format.
  3. Validates the response against the model.
  4. Returns an Event instance in parsed_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 JSONStructured outputs (schema)messages.parse() + Pydantic
Valid JSON guaranteedNoYesYes
Schema enforcedNoYesYes
Parsing code neededYes, defensivejson.loadsNone
Result typedictdictTyped model instance
Best forQuick experiments, models or platforms without structured outputsSchemas from files or other systemsPython 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

  1. Contact defines the exact structure: four string fields. Its schema is sent automatically by messages.parse().
  2. 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.
  3. stop_reason is checked before using parsed_output, because a truncated or refused response cannot be trusted.
  4. contact.model_dump() converts the model into a dictionary, and the comprehension keeps only fields with values, giving a compact display.
  5. 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_reason before 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

  1. Use prompt-based JSON to extract a book's title, author, and year from a sentence. Parse it with extract_json() and validate it.
  2. Rewrite step 1 using a raw JSON schema in output_config.format.
  3. Rewrite step 1 again using a Pydantic model and messages.parse(). Which version needs the least code?
  4. Add a field "role": str (job title) to Contact, and test it on the three sample texts. How is the missing role represented?
Show a possible solution for step 3
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.

0 Comments

Reviewed before they appear

No comments yet.

Anthropic SDK Using Python
Anthropic SDK Intro Claude API Intro SDK Requirements Install Anthropic SDK Upgrade the SDK Create an API Key Set API Keys Use Environment Variables Import Anthropic Create a Client Send Your First Message Choose a Claude Model Set Max Tokens Read a Response Print Response Text Understand Message Blocks Use System Prompts Send User Messages Set Temperature Handle Stop Reasons Messages API Intro Create Messages Use Message Roles Send Conversation History Continue a Conversation Build a Chat Loop Reset Chat History Use Multiple Turns Inspect Message IDs Read Usage Data Write Clear Prompts Set a System Prompt Add Context to Prompts Use Prompt Templates Add Examples to Prompts Request JSON Output Use XML Tags Control Response Length Improve Prompt Quality Avoid Prompt Injection Read Text Blocks Read Content Blocks Check Stop Reasons Count Input Tokens Count Output Tokens Count Tokens Before Sending Limit Output Tokens Use Stop Sequences Read Request IDs Parse API Responses Streaming Intro Stream Text Output Use Stream Events Use Stream Helpers Get Final Messages Stream Async Responses Handle Stream Errors Build a Live Chat Stream Long Responses Close Streams Safely Build a Chatbot Keep Chat History Manage Chat Sessions Add Chat Memory Build a CLI Chatbot Build a Web Chatbot Use Flask with Claude Use FastAPI with Claude Return JSON from an API Add Chat Error Messages Async SDK Intro Create an Async Client Send Async Messages Use asyncio Run Parallel Requests Stream Async Text Handle Async Errors Set Async Timeouts Close Async Clients Choose Sync or Async Tool Use Intro Define a Tool Create Tool Schemas Send Tools to Claude Read Tool Calls Run Python Tools Return Tool Results Continue After Tool Use Control Tool Choice Handle Tool Errors Use the beta_tool Decorator Create a Tool Runner Add Tool Docstrings Validate Tool Inputs Use Multiple Tools Limit Tool Calls Log Tool Activity Build a Weather Tool Build a Search Tool Build a Calculator Tool Structured Output Intro Request JSON Data Validate JSON Output Use Pydantic Models Handle Invalid JSON Extract Fields from Text Generate Typed Data Use Image URLs Send Base64 Images Read PDFs with Claude Use Output Schemas Build Data Extraction Test Structured Responses Send Image Inputs Upload Files Use File IDs Process Text Files Summarize Documents Extract Table Data Handle File Errors Read Cache Usage Reduce Repeated Input Costs Debug Cache Misses Prompt Caching Intro Enable Prompt Caching Cache System Prompts Cache Long Context Test Cache Performance Use Cache Breakpoints Cache Best Practices Message Batches Intro Create a Message Batch Set Batch IDs Check Batch Status List Message Batches Get Batch Results Process Batch Outputs Handle Failed Batch Items Use Batches for Bulk Tasks Compare Batches and Requests Handle API Errors Handle Rate Limits Retry Failed Requests Set Request Timeouts Handle Network Errors Use Exponential Backoff Build Reliable API Calls Use Extended Thinking Log API Errors Track Request IDs Paginate API Results Set Thinking Budgets Read Thinking Blocks Use Tool Search Use Web Search Tools Use Computer Tools Use Citations Use Token Counting Read Raw Responses Set Custom Headers Use Claude on AWS Bedrock Use Claude on Vertex AI Use Claude on Microsoft Foundry Configure Cloud Credentials Choose a Cloud Provider Compare API Integrations Configure Provider Clients Handle Cloud Errors Use Cloud SDK Extras Deploy Cloud Apps Protect API Keys Use a .env File Ignore Secrets in Git Validate User Input Reduce Prompt Injection Limit Tool Permissions Avoid Unsafe Tool Calls Sanitize Tool Results Set Usage Limits Review Security Logs Test SDK Functions Mock API Responses Test Error Handling Test Streaming Code Measure Token Usage Track API Costs Configure Logging Deploy with Docker Deploy a FastAPI App Monitor Production Apps Build a Claude Chatbot Build a Text Summarizer Build a Code Explainer Build a JSON Extractor Build a PDF Assistant Build a Streaming Chat App Build a Tool-Calling Agent Build a Batch Processor Build a Research Assistant Build a Claude API Dashboard
Ask about this post
AI Ask about this post

Ask questions about Request JSON Output and get answers drawn from it.

Signed-in readers only.