Request JSON Data
Requesting JSON with a Raw Schema
messages.parse() with a Pydantic model is the most convenient way to get structured output in Python (Lessons 101 and 104). Sometimes, though, your schema is not a Python class:
- it lives in a JSON file shared with other services or languages,
- it comes from an OpenAPI specification,
- it is built at runtime, for example from a form builder or database configuration.
In these cases, send the schema directly with messages.create() and the output_config parameter:
response = client.messages.create(
model="claude-opus-5-5",
max_tokens=2048,
messages=[{"role": "user", "content": "..."}],
output_config={
"format": {
"type": "json_schema",
"schema": {
"type": "object",
"properties": {
"name": {"type": "string"},
"email": {"type": "string"},
},
"required": ["name", "email"],
"additionalProperties": False,
},
}
},
)
| Part | Meaning |
|---|---|
output_config | Output settings for the request (it also holds effort, Lesson 47) |
"format" | The output format to enforce |
"type": "json_schema" | The format is defined by a JSON Schema |
"schema" | The JSON Schema itself |
Note: an older form of this feature used a top-level
output_formatparameter with a beta header. According to the documentation,output_formatis deprecated in favor ofoutput_config.format, and the Python SDK (v1.0 and later) rejectsoutput_format={...}onclient.beta.messages.create()with aTypeError. Useoutput_config. (messages.parse()still acceptsoutput_format=as a convenience for passing a Pydantic model, and translates it internally.)
Reading the JSON
The JSON arrives in the response's text block. Find the text block by type, because a thinking block may come first (Lesson 16), and parse it:
import json
text = next(block.text for block in response.content if block.type == "text")
data = json.loads(text)
With the raw-schema approach, you get a plain Python dict. Unlike messages.parse(), the SDK does not validate the result against extra constraints for you. Lesson 103 covers that.
Writing Schemas for JSON Outputs
Objects, Arrays, and Nesting
{
"type": "object",
"properties": {
"title": {"type": "string"},
"topics": {"type": "array", "items": {"type": "string"}},
"instructor": {
"type": "object",
"properties": {
"name": {"type": "string"},
"experience_years": {"type": "integer"}
},
"required": ["name", "experience_years"],
"additionalProperties": false
}
},
"required": ["title", "topics", "instructor"],
"additionalProperties": false
}
Add "additionalProperties": false to every object, so no unexpected fields appear.
Fixed Values with enum
"level": {"type": "string", "enum": ["beginner", "intermediate", "advanced"]}
Values That May Be Missing: null vs. Optional
Information is often missing from the input. There are two ways to represent that:
| Approach | Schema | Output when the value is missing |
|---|---|---|
| Nullable required field | "type": ["string", "null"] and listed in required | "prerequisites": null |
| Optional field | Not listed in required | The field is absent |
Nullable required fields make the output's shape predictable: every key is always present. However, type arrays such as ["string", "null"] count as union types, which the documentation limits to 16 per request across all strict schemas, because they are expensive to compile. Optional fields count toward a separate limit of 24 optional parameters per request (Lesson 108).
Property Order
According to the documentation, output properties keep the order of your schema, with one caveat: required properties come first, then optional properties. If order matters to your application, for example when writing CSV columns, make all properties required, or reorder them in code.
Unsupported Constraints and transform_schema()
JSON outputs support standard JSON Schema with some limitations. For example, numeric and length constraints such as minimum, maximum, minLength, and maxLength are not enforced by the API. Sending a schema with unsupported features can fail.
The SDK provides transform_schema(), which prepares a schema for the API, following the same steps the SDK's helpers use:
- remove unsupported constraints, such as
minimumandmaxLength, - add each removed constraint to the field's description, so Claude still sees the rule as guidance,
- add
additionalProperties: falseto all objects, and - filter string formats to the supported list.
from anthropic import transform_schema
sent_schema = transform_schema(original_schema) # Also accepts a Pydantic model
transform_schema() returns the transformed schema instead of sending it, so you can inspect it, and edit it if needed. Keep the original schema for validation in your code (Lesson 103), because the transformed version no longer contains the removed constraints.
Practical Example: A Schema from a File
This example extracts structured course data from a free-text course description, using a JSON Schema stored in a file, the form in which another team or service might share it.
course_schema.json
{
"type": "object",
"properties": {
"title": {"type": "string", "description": "Course title"},
"level": {"type": "string", "enum": ["beginner", "intermediate", "advanced"]},
"duration_weeks": {"type": "integer", "minimum": 1, "maximum": 52,
"description": "Course length in weeks"},
"topics": {"type": "array", "items": {"type": "string"},
"description": "Main topics covered, 3 to 6 short items"},
"prerequisites": {"type": ["string", "null"],
"description": "Required prior knowledge, or null if none is required"},
"price_inr": {"type": "integer", "description": "Price in Indian rupees; omit if not mentioned"}
},
"required": ["title", "level", "duration_weeks", "topics", "prerequisites"],
"additionalProperties": false
}
raw_schema.py
"""raw_schema.py: request JSON data with a raw JSON schema loaded from a file."""
import json
from pathlib import Path
from anthropic import Anthropic, transform_schema
client = Anthropic()
ORIGINAL_SCHEMA = json.loads(Path("course_schema.json").read_text(encoding="utf-8"))
SENT_SCHEMA = transform_schema(ORIGINAL_SCHEMA)
DESCRIPTION = """Build real web apps with Django! In this 8-week program you'll learn models,
views, templates, forms, and deploying to the cloud. You should already be comfortable with
basic Python (variables, loops, functions). Perfect for learners moving past the basics."""
def extract_course(description: str) -> dict | None:
response = client.messages.create(
model="claude-opus-5-5",
max_tokens=2048,
messages=[{"role": "user", "content":
f"Extract the course details.\n<description>\n{description}\n</description>"}],
output_config={"format": {"type": "json_schema", "schema": SENT_SCHEMA}},
)
if response.stop_reason != "end_turn":
print(f"[no result: stop_reason={response.stop_reason}]")
return None
text = next(block.text for block in response.content if block.type == "text")
return json.loads(text)
if __name__ == "__main__":
print("duration_weeks as sent:", json.dumps(SENT_SCHEMA["properties"]["duration_weeks"]))
course = extract_course(DESCRIPTION)
print(json.dumps(course, indent=2, ensure_ascii=False))
Example output (the transformed description text and extracted values may differ slightly):
duration_weeks as sent: {"type": "integer", "description": "Course length in weeks {minimum: 1, maximum: 52}"}
{
"title": "Build Real Web Apps with Django",
"level": "intermediate",
"duration_weeks": 8,
"topics": [
"Models",
"Views",
"Templates",
"Forms",
"Cloud deployment"
],
"prerequisites": "Basic Python: variables, loops, and functions"
}
How It Works
- The schema is loaded from a file, so other services or languages can share the same definition.
ORIGINAL_SCHEMAis kept unchanged for validation later. transform_schema()prepares the schema for the API: the unsupportedminimumandmaximumconstraints onduration_weeksare removed from the schema and mentioned in the field's description instead, so Claude still sees them as guidance. The first printed line shows the transformed field.output_config={"format": {"type": "json_schema", "schema": SENT_SCHEMA}}asks for JSON that matches the schema.- The stop reason is checked before parsing, because refusals and truncated outputs are not guaranteed to match the schema.
next(...)finds the first text block, skipping any thinking block, andjson.loads()turns it into a dictionary.- Required fields come first:
prerequisitesis required, and nullable, so it is always present.price_inris optional, and is absent because the description does not mention a price.
Common Mistakes
Using the Deprecated output_format Parameter
What happens: 400 errors without the old beta header, or a TypeError in the Python SDK.
Fix: use output_config={"format": {...}}.
Reading content[0]
What happens: AttributeError when the first block is a thinking block.
Fix: find the text block by type.
Forgetting additionalProperties: false
What happens: objects may contain unexpected extra keys.
Fix: add it to every object, or use transform_schema(), which adds it for you.
Assuming Removed Constraints Are Enforced
What happens: values outside minimum/maximum reach your code, because the API only sees them as description text.
Fix: validate against the original schema (Lesson 103).
Depending on Property Order
What happens: code that expects fields in schema order breaks when optional fields come after required ones.
Fix: access fields by name, or make all fields required.
Best Practices
- Use raw schemas when the schema comes from outside Python code.
- Send schemas through
transform_schema(), and keep the original for validation. - Add
additionalProperties: falseto every object. - Choose nullable or optional fields deliberately, keeping complexity limits in mind.
- Find the text block by type, and check
stop_reasonbefore parsing. - Describe fields clearly, including units and formats.
Exercise
- Add a nested
instructorobject (name,experience_years) tocourse_schema.json, and test it with a description that mentions an instructor. - Change
prerequisitesfrom nullable-required to optional. How does the output change for a course without prerequisites? - Print the full output of
transform_schema(ORIGINAL_SCHEMA)and compare it with the original. List every difference. - Build the schema in Python code from a list of field names and types, instead of a file.
Step 2: with prerequisites optional (removed from required, type "string"), Claude can omit it when there are no prerequisites. The key is then absent instead of null, so code must use course.get("prerequisites") instead of course["prerequisites"]. Optional fields also move after the required fields in the output.
Step 4:
FIELDS = [("title", "string", "Course title"),
("duration_weeks", "integer", "Course length in weeks"),
("is_free", "boolean", "Whether the course is free")]
schema = {
"type": "object",
"properties": {name: {"type": kind, "description": text} for name, kind, text in FIELDS},
"required": [name for name, _, _ in FIELDS],
"additionalProperties": False,
}
Building schemas in code lets an application define extraction fields from configuration, for example a form designer where users choose which fields to extract.