Validating and Parsing Structured Responses in Python
Defining a schema is only half the job - you also need to turn the response you get back into a usable Python object, and know what to do when that response isn't complete.
Search across all documentation pages
Defining a schema is only half the job - you also need to turn the response you get back into a usable Python object, and know what to do when that response isn't complete.
This page covers client.messages.parse(), the Pydantic-backed helper that does the validation and deserialization for you, plus the manual path with client.messages.create() for when you need more control.
client.messages.parse() is the recommended way to consume a structured output in Python: pass it a schema (typically from a Pydantic model), and it returns a response whose parsed_output attribute is already a validated instance of that model.
This replaces the manual sequence of extracting text from the response, calling json.loads(), and then constructing your own object from the dict.
You can still use client.messages.create() directly when you want the raw response content, for example to inspect stop_reason before deciding whether the output is even safe to parse.
Both paths use the same output_config.format schema underneath - parse() is a convenience layer, not a different API feature.
Quick-reference recipe card - copy-paste ready.
from pydantic import BaseModel
from anthropic import Anthropic
class Invoice(BaseModel):
vendor: str
total: float
currency: str
client = Anthropic()
response = client.messages.parse(
model="claude-sonnet-5",
max_tokens=1024,
output_config={"format": {"type": "json_schema", "schema": Invoice.model_json_schema()}},
messages=[{"role": "user", "content": "Extract: Acme Corp billed $1,240.50 USD."}],
)
invoice: Invoice = response.parsed_output
print(invoice.vendor, invoice.total, invoice.currency)When to reach for this:
from pydantic import BaseModel, ValidationError
from anthropic import Anthropic, APIStatusError
class SupportTicket(BaseModel):
subject: str
urgency: str
customer_email: str
client = Anthropic()
def extract_ticket(raw_text: str) -> SupportTicket | None:
try:
response = client.messages.parse(
model="claude-sonnet-5",
max_tokens=1024,
output_config={
"format": {"type": "json_schema", "schema": SupportTicket.model_json_schema()}
},
messages=[{"role": "user", "content": f"Extract ticket fields from:\n\n{raw_text}"}],
)
except APIStatusError as e:
print(f"API error: {e.status_code} {e.message}")
return None
if response.stop_reason == "max_tokens":
print("Response was truncated before completing - not safe to trust.")
return None
if response.stop_reason == "refusal":
print("Claude declined to produce a response for this input.")
return None
return response.parsed_output
ticket = extract_ticket(
"From: jane@example.com\nSubject: Can't log in\n\n"
"I've been locked out of my account since this morning, please help ASAP."
)
if ticket:
print(ticket.subject, ticket.urgency, ticket.customer_email)What this demonstrates:
stop_reason is checked before trusting response.parsed_output, because a truncated or refused response is not guaranteed to have a usable parsed value.APIStatusError catches network/API-level failures separately from content-level issues like truncation or refusal.None on any failure path rather than letting an exception from a partially-formed response propagate unexpectedly.response.parsed_output is only read once every earlier guard has passed.client.messages.parse() sends the same request as client.messages.create(), with the schema built from your Pydantic model (or raw dict) passed via output_config.format.response.parsed_output.response object still has the same fields as a normal Message - stop_reason, usage, content - parsed_output is additive, not a replacement.client.messages.create() | client.messages.parse() | |
|---|---|---|
| Returns | Raw Message with text content | Message plus a validated parsed_output |
| You still do | json.loads() and manual construction | Nothing extra - already an object |
| Typical use | You need raw text, or want manual control over parsing | You have a Pydantic model and want the finished object |
import json
from anthropic import Anthropic
client = Anthropic()
schema = {
"type": "object",
"properties": {"summary": {"type": "string"}},
"required": ["summary"],
"additionalProperties": False,
}
response = client.messages.create(
model="claude-sonnet-5",
max_tokens=64, # deliberately small - likely to truncate on a long answer
output_config={"format": {"type": "json_schema", "schema": schema}},
messages=[{"role": "user", "content": "Write an extremely detailed multi-paragraph summary."}],
)
raw_text = response.content[0].text
if response.stop_reason == "max_tokens":
print("Truncated - do not attempt json.loads() on this, retry with more max_tokens instead.")
else:
data = json.loads(raw_text)
print(data["summary"])create() directly here makes the truncation check explicit and visible, which is useful when you want full control over the failure path rather than relying on parse()'s internal handling.json.loads() on a known-truncated string will raise json.JSONDecodeError - checking stop_reason first avoids hitting that exception in the normal control flow.from pydantic import BaseModel, field_validator
class Invoice(BaseModel):
vendor: str
total: float
@field_validator("total")
@classmethod
def total_must_be_positive(cls, v: float) -> float:
if v < 0:
raise ValueError("total must be non-negative")
return vclient.messages.parse() constructs parsed_output - a schema-valid response that fails a custom validator raises a pydantic.ValidationError, which is a distinct failure mode from a malformed API response.response.parsed_output before checking stop_reason. On a truncated response, the parsed value may be missing or the parse step itself may have failed. Fix: check stop_reason != "max_tokens" (and != "refusal") before touching parsed_output.parse() and create() need different schemas. They use the exact same output_config.format schema - the only difference is what the SDK does with the response afterward. Fix: define the schema once and pass it to whichever call you use.pydantic.ValidationError when you have custom validators on the model. A schema-valid response can still fail a Pydantic-level constraint like a custom field_validator. Fix: wrap the parse() call in a try/except that catches both API errors and Pydantic validation errors if your model has custom validation logic.create() still returns text you must json.loads() yourself. Developers sometimes expect create() to auto-parse the way parse() does. Fix: use parse() when you want the object; use create() only when you specifically want the raw text or more manual control over the response handling.| Alternative | Use When | Don't Use When |
|---|---|---|
client.messages.parse() | You have a Pydantic model and want a validated object in one call | You need to inspect raw response text or stream the response |
client.messages.create() + manual json.loads() | You want full control over the parsing and error-handling flow | You just want the finished object with minimal code |
Hand-rolled JSON extraction from free text (no output_config.format) | Legacy code you haven't migrated yet | Any new integration - offers no reliability guarantee over structured outputs |
output_config.format schema.parse() additionally validates and deserializes the response into response.parsed_output; create() leaves you to call json.loads() yourself.model_json_schema() generates the schema and gives you a typed return value.parse(), but you lose the typed object convenience - parsed_output becomes a plain dict in that case.response object before relying on parsed_output, the same way you would with create().max_tokens or refusal stop reason means the content may not be complete or usable, regardless of which method you called.anthropic.APIStatusError and its subclasses.pydantic.ValidationError - catch both if your model has custom validation.stop_reason and handle exceptions before assuming it's populated.response object returned by parse() still carries the normal content field with the raw text blocks, in addition to parsed_output.parse() does not automatically retry with a larger max_tokens. You need to detect truncation via stop_reason and implement your own retry logic.field_validator or model_validator on your Pydantic model runs after the JSON Schema check passes, so it can still reject values the schema itself allowed.parse() only adds a client-side validation/deserialization step after the response arrives, which is negligible compared to network latency.dict matching the schema's shape, rather than a typed model instance.data = json.loads(raw_text)
invoice = Invoice.model_validate(data)Model.model_validate(data) does the same validation step parse() does internally, useful if you're on the manual create() path but still want a typed object.Stack versions: Written against the Claude model lineup current as of ~June 2026 - Claude Fable 5, Claude Opus 4.8, Claude Sonnet 5 (the default), and Claude Haiku 4.5 - and the official
anthropicPython SDK (latest 0.x release). Model names, SDK versions, and pricing move quickly - verify current specifics at platform.claude.com/docs before relying on them.
Reviewed by Chris St. John·Last updated Jul 13, 2026