Claude Structured Output: JSON That Always Parses
"Reply only with JSON" is a prompt, not a guarantee — Claude can still wrap it in prose, trail a comma, or drop a field, and your json.loads() throws. Anthropic's Structured Outputs feature removes the guesswork: pass a JSON Schema in the output_config.format request field and Claude's response is constrained at sampling time to match it, or use strict: true on a tool so its parameters always validate. The SDKs add a client.messages.parse() helper that takes a Pydantic or Zod model and hands back a typed, validated object. This guide covers the three approaches — prompting vs strict tool use vs structured outputs — with working code, the JSON Schema limits you must respect, and when each one is the right call.
How to get valid JSON from Claude, every time
Stop prompting for JSON and start constraining it. Anthropic's Structured Outputs feature gives you two enforcement mechanisms, not a hopeful instruction:
- JSON outputs — add an
output_config.formatfield (type: "json_schema") to yourmessages.createcall, and Claude's response text is constrained to match your JSON Schema. - Strict tool use — add
strict: trueto a tool definition, and the arguments Claude passes to that tool are guaranteed to validate against itsinput_schema.
Both are generally available — no beta header required — and the SDKs wrap them in a client.messages.parse() helper that takes a Pydantic model (Python) or a Zod schema (TypeScript) and returns a typed, already-validated object. The rest of this guide shows the code, the schema limits, and when each mechanism is the right tool.
Why 'reply only in JSON' keeps breaking
Because a prompt is a request, and generation is still free — nothing stops the model from drifting off-format. When you ask Claude to "respond with a JSON object and nothing else," every token is still sampled from the full vocabulary, so any of these can land in production:
- Prose wrappers — "Sure, here is the JSON:" before the object, or a closing explanation after it.
- Trailing commas and unescaped quotes — syntactically invalid JSON that your parser rejects.
- Type drift — a number returned as a string, a boolean as
"yes", an enum value the caller does not expect. - Missing or extra keys — a required field silently dropped, or an unrequested one added.
You end up bolting on regex extraction, try/except guards, and retry loops — and still get pages that occasionally crash. Structured outputs move the constraint from the prompt into the sampler, so the failure mode disappears by construction rather than being caught after the fact.
How Claude structured outputs actually work
Claude compiles your schema into a grammar and only samples tokens that keep the output valid. When you send a schema, Anthropic's platform turns it into a formal grammar that constrains generation token by token — the model literally cannot emit a character that would break the schema. Two operational details follow from that, both from the primary docs:
- First-request compile cost. The first request with a new schema incurs additional latency while the grammar compiles. Compiled grammars are then cached for 24 hours from last use, so repeat calls are not slowed (Anthropic docs).
- Cache invalidation. The cache is invalidated if the schema structure or the set of tools changes — but changing only
nameordescriptionfields does not invalidate it, so you can tweak wording freely.
Secondary write-ups put the first-request overhead at roughly 100–300ms; the primary docs confirm there is added latency but do not state that exact figure, so treat it as an estimate rather than a spec.
Working example: a schema-constrained JSON response
The whole feature is one extra field on the request you already send. Here Claude extracts structured data from a support ticket and returns JSON that is valid against the schema by construction. Note additionalProperties: false — it is required, not optional:
import anthropic
client = anthropic.Anthropic(
base_url="https://www.datallmlab.com/v1", # DataLLM Lab gateway
api_key="YOUR_DATALLMLAB_KEY",
)
schema = {
"type": "object",
"properties": {
"category": {"type": "string", "enum": ["billing", "bug", "feature"]},
"priority": {"type": "string", "enum": ["low", "med", "high"]},
"summary": {"type": "string"},
},
"required": ["category", "priority", "summary"],
"additionalProperties": False, # must be false
}
msg = client.messages.create(
model="claude-opus-4.8",
max_tokens=512,
messages=[{"role": "user", "content": "Card declined three times, cannot check out."}],
output_config={"format": {"type": "json_schema", "schema": schema}},
)
# msg.content is now valid JSON matching the schema — no parsing guards needed
import json
data = json.loads(msg.content[0].text) # {"category": "billing", "priority": "high", ...}
The gateway base URL keeps the exact same OpenAI/Anthropic-compatible shape — see our OpenAI-compatible API guide for how one endpoint fronts many providers. The output_config.format field is passed straight through to Claude.
The messages.parse() helper does the validation for you
If you already model your data with Pydantic or Zod, skip the raw schema — hand the model class in and get a typed object back. The SDKs expose client.messages.parse(), which auto-transforms your schema (adding additionalProperties: false for you), sends it as output_config.format, and validates the response against your original model before returning a parsed_output:
from pydantic import BaseModel
from typing import Literal
import anthropic
class Ticket(BaseModel):
category: Literal["billing", "bug", "feature"]
priority: Literal["low", "med", "high"]
summary: str
client = anthropic.Anthropic(base_url="https://www.datallmlab.com/v1")
msg = client.messages.parse(
model="claude-opus-4.8",
max_tokens=512,
messages=[{"role": "user", "content": "Card declined three times, cannot check out."}],
output_format=Ticket, # Pydantic model in, typed object out
)
ticket: Ticket = msg.parsed_output # already validated — ticket.priority == "high"
In TypeScript the same helper takes zodOutputFormat(YourZodSchema, "name") or jsonSchemaOutputFormat(...) instead of a Pydantic model. The Python output_format= convenience param is translated internally to output_config.format, so you never touch the raw request shape.
Strict tool use vs JSON output — pick by what you are constraining
JSON outputs control what Claude says; strict tool use controls how Claude calls your functions. They are the two halves of the same feature, and the right one depends on whether the JSON is your answer or your function arguments:
- You want a JSON answer (extraction, classification, a structured summary) → use
output_config.format. Claude's reply text is the JSON. - You are calling functions (an agent invoking tools) → add
strict: trueto the tool. TheinputClaude passes is guaranteed to match the tool'sinput_schema— no more malformed arguments crashing your handler.
Strict tool use is what you want inside agent loops; a whole class of "the model called my tool with a string where I needed an integer" bugs simply stops happening. If you are building agents, our best LLM for AI agents guide covers where reliable tool-calling sits in the wider decision. Just like JSON outputs, the tool's input_schema must include additionalProperties: false.
Prompting vs tool use vs structured outputs
Three ways to get JSON out of Claude, in ascending order of guarantee. The honest trade-offs:
| Approach | How | Guarantee | Best for | Watch out for |
|---|---|---|---|---|
| Reply-in-JSON prompting | "Respond only with JSON" in the prompt | None — best-effort | Quick prototypes, unsupported models | Prose wrappers, invalid JSON, missing keys |
| Strict tool use | strict: true on a tool | Args match input_schema | Agents, function calling | Schema needs additionalProperties: false |
| JSON outputs | output_config.format / messages.parse() | Response matches schema | Extraction, classification, structured replies | First-request compile latency; JSON Schema subset |
The two strict paths differ only in what they constrain — Claude's spoken response versus its tool-call arguments. Prompting is the fallback, not the default: reach for it only when a model or endpoint does not support structured outputs.
JSON Schema limits you have to respect
Claude supports a subset of JSON Schema — knowing the boundary saves a confusing error. Because the schema compiles to a sampling grammar, some keywords cannot be expressed. From the primary Structured Outputs reference:
| Supported | Not supported |
|---|---|
| Basic types: object, array, string, integer, number, boolean, null | Recursive schemas |
enum (scalars only), const | Complex enum types |
anyOf / allOf (with limits; allOf + $ref unsupported) | External $ref |
$ref / $def / definitions | Numeric constraints: minimum, maximum, multipleOf |
default, required, additionalProperties: false | String length: minLength, maxLength |
String format: date-time, date, time, duration, email, hostname, uri, ipv4, ipv6, uuid | Array constraints beyond minItems 0/1 |
Array minItems of 0 or 1 only | additionalProperties set to anything but false |
The practical rule: express constraints the grammar cannot enforce — value ranges, string lengths, regex — as post-validation in your own code or in your Pydantic/Zod model, and keep the schema you send to Claude within the supported set. The messages.parse() helper validates the response against your original model, so those richer constraints are still checked client-side.
When not to use structured outputs
Enforcement is not free, and not every task needs it. Skip it when:
- You want prose. A chat reply, an explanation, a written summary — forcing it through a schema fights the model. Structured outputs are for machine-consumed data, not human-read text.
- Your model is not on the supported list. Per the July 2026 docs the list spans Opus 4.5–4.8, Sonnet 4.5/4.6/5, Haiku 4.5, and the newer Fable 5 / Mythos 5 lines — but it changes, so verify before pinning. On an unsupported model, prompt-plus-guards is your only path. If you are weighing models, see Sonnet vs Opus.
- The shape needs recursion or numeric bounds the grammar can't express. A deeply nested tree or a "score between 0 and 100" constraint has to live in client-side validation regardless.
- You are one request in and latency-sensitive. The first-schema compile adds latency; for a single one-off call the prompt route may be simpler. At any real volume the 24-hour grammar cache erases that cost.
There is also a separate Agent SDK path for structured output from agents, distinct from the Messages-API output_config.format feature — reach for that when your structure is the output of a whole agent run, not a single completion.
Structured outputs on 300+ models, one key
DataLLM Lab fronts Claude's output_config.format and strict tool use through an OpenAI-compatible endpoint — so you get schema-valid JSON from Claude and fail over to an equivalent model without changing your code.
FAQ
How do I get guaranteed valid JSON from Claude?
Use Structured Outputs: pass a JSON Schema in output_config.format (type json_schema) so the response is constrained at sampling time, or use client.messages.parse() with a Pydantic/Zod model to get a typed, validated object. Far more reliable than prompting for JSON.
output_config.format vs strict tool use — what is the difference?
output_config.format controls Claude's response format (what Claude says as JSON). strict: true on a tool validates tool-call arguments against the tool's input_schema. Use the first for JSON answers, the second inside agent/function-calling loops.
Do structured outputs need a beta header?
No — they are generally available and need no beta header. The old anthropic-beta: structured-outputs-2025-11-13 header and the output_format parameter still work during a transition period, but the parameter has moved to output_config.format.
What JSON Schema features are supported?
A subset: basic types, enum/const, anyOf/allOf with limits, $ref/$def, default/required, several string formats, and additionalProperties: false. Not supported: recursion, external $ref, numeric bounds, string-length constraints.
Which Claude models support structured outputs?
Per July 2026 docs: Opus 4.8/4.7/4.6/4.5, Sonnet 5/4.6/4.5, Haiku 4.5, plus Fable 5, Mythos 5 and Mythos Preview. The list is broader than the earlier beta (Sonnet 4.5 + Opus 4.1); check the docs before pinning a model.
Are structured outputs slower?
Only the first request with a new schema, while the grammar compiles. Compiled grammars are cached for 24 hours from last use; the cache is invalidated only if the schema structure or tool set changes, not on name/description edits.
Can I still just prompt Claude to reply in JSON?
Yes, but it is the least reliable option — no enforcement, so you get occasional prose, type drift, or invalid JSON and need guards and retries. Reserve it for prototypes or models that do not support structured outputs.
DataLLM Lab