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.
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.
Summary
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.
Recipe
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:
- You already have (or want) a Pydantic model representing the data you're extracting.
- You want a typed object back, not a dict you have to index into.
- You're building a pipeline where the parsed value gets passed directly into other typed Python code.
- You want validation and parsing to happen in one step instead of two.
Working Example
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_reasonis checked before trustingresponse.parsed_output, because a truncated or refused response is not guaranteed to have a usable parsed value.APIStatusErrorcatches network/API-level failures separately from content-level issues like truncation or refusal.- The function returns
Noneon any failure path rather than letting an exception from a partially-formed response propagate unexpectedly. response.parsed_outputis only read once every earlier guard has passed.
Deep Dive
How It Works
client.messages.parse()sends the same request asclient.messages.create(), with the schema built from your Pydantic model (or raw dict) passed viaoutput_config.format.- Once the response comes back, the SDK validates the returned JSON text against the schema and constructs an instance of the model you provided, exposing it as
response.parsed_output. - The underlying
responseobject still has the same fields as a normalMessage-stop_reason,usage,content-parsed_outputis additive, not a replacement. - Validation at this layer confirms the response matches the shape the schema described; it does not verify the values are factually correct.
create() vs. parse()
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 |
Handling a Response That Didn't Parse Cleanly
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"])- Using
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 onparse()'s internal handling. - Calling
json.loads()on a known-truncated string will raisejson.JSONDecodeError- checkingstop_reasonfirst avoids hitting that exception in the normal control flow.
Python Notes
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 v- Pydantic validators still run when
client.messages.parse()constructsparsed_output- a schema-valid response that fails a custom validator raises apydantic.ValidationError, which is a distinct failure mode from a malformed API response. - This is useful for constraints the JSON Schema layer doesn't enforce (like numeric ranges), letting Pydantic catch what the schema-level guarantee can't.
Gotchas
- Reading
response.parsed_outputbefore checkingstop_reason. On a truncated response, the parsed value may be missing or the parse step itself may have failed. Fix: checkstop_reason != "max_tokens"(and!= "refusal") before touchingparsed_output. - Assuming
parse()andcreate()need different schemas. They use the exact sameoutput_config.formatschema - 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. - Not catching
pydantic.ValidationErrorwhen you have custom validators on the model. A schema-valid response can still fail a Pydantic-level constraint like a customfield_validator. Fix: wrap theparse()call in atry/exceptthat catches both API errors and Pydantic validation errors if your model has custom validation logic. - Treating a successful parse as proof the data is correct. Validation confirms shape and any Pydantic-level constraints, not that the extracted values are factually accurate. Fix: add a downstream review or spot-check step for anything high-stakes.
- Forgetting that
create()still returns text you mustjson.loads()yourself. Developers sometimes expectcreate()to auto-parse the wayparse()does. Fix: useparse()when you want the object; usecreate()only when you specifically want the raw text or more manual control over the response handling.
Alternatives
| 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 |
FAQs
What's the actual difference between create() and parse()?
- Both send the same request shape with the same
output_config.formatschema. parse()additionally validates and deserializes the response intoresponse.parsed_output;create()leaves you to calljson.loads()yourself.
Do I need a Pydantic model to use parse()?
- A Pydantic model is the common pattern because
model_json_schema()generates the schema and gives you a typed return value. - A raw schema dict also works with
parse(), but you lose the typed object convenience -parsed_outputbecomes a plain dict in that case.
Should I check stop_reason before or after calling parse()?
- Check it on the returned
responseobject before relying onparsed_output, the same way you would withcreate(). - A
max_tokensorrefusalstop reason means the content may not be complete or usable, regardless of which method you called.
What exception types should I catch around a parse() call?
- API-level failures (network, auth, rate limits) surface as
anthropic.APIStatusErrorand its subclasses. - Content-level issues like a custom Pydantic validator failing surface as
pydantic.ValidationError- catch both if your model has custom validation.
Is response.parsed_output guaranteed to be non-None?
- Only when the response completed successfully and passed validation - always check
stop_reasonand handle exceptions before assuming it's populated.
Can I still access the raw text alongside parsed_output?
- Yes - the
responseobject returned byparse()still carries the normalcontentfield with the raw text blocks, in addition toparsed_output.
Does parse() retry automatically on truncation?
- No -
parse()does not automatically retry with a largermax_tokens. You need to detect truncation viastop_reasonand implement your own retry logic.
Can Pydantic validators reject a response that's schema-valid?
- Yes - a
field_validatorormodel_validatoron your Pydantic model runs after the JSON Schema check passes, so it can still reject values the schema itself allowed.
Is it slower to use parse() instead of create()?
- The API call itself is identical -
parse()only adds a client-side validation/deserialization step after the response arrives, which is negligible compared to network latency.
What does parsed_output look like if I pass a raw dict schema instead of a Pydantic model?
- It's a plain Python
dictmatching the schema's shape, rather than a typed model instance. - Use a Pydantic model when you want attribute access and static typing instead of dict indexing.
Can I reuse the same Pydantic model for both parse() and manual json.loads()?
data = json.loads(raw_text)
invoice = Invoice.model_validate(data)- Yes -
Model.model_validate(data)does the same validation stepparse()does internally, useful if you're on the manualcreate()path but still want a typed object.
Related
- Structured Outputs Basics - the first introduction to client.messages.parse.
- Defining a JSON Schema for output_config.format - how the schema this page parses is built.
- Handling Malformed or Truncated JSON Output - a deeper look at the stop_reason checks used throughout this page.
- Structured Output Field Types and Constraints Reference - what schema constructs are safe to rely on before you get to parsing.
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.