Claude API Structured JSON Output Schema Guide
Claude doesn't have a dedicated "JSON mode" flag like some other APIs, but you can reliably get structured, schema-conformant JSON output using tool definitions. The most robust method is to define a tool with an input schema and force Claude to call it — this constrains the model's output to match your schema rather than hoping a prompt instruction works every time.
There's also a lighter-weight approach: prompting Claude directly to return JSON and validating the response yourself. Both methods work, but they solve different problems. This article covers when to use each, how to define schemas correctly, and how to handle the edge cases that break naive JSON parsing in production.
Why "just ask for JSON" often fails
A common first attempt looks like this:
Return the extracted data as JSON with keys "name", "email", "age".
This works most of the time, but it fails in predictable ways:
- The model wraps the JSON in a markdown code fence (
`json ...`) - It adds a sentence before or after the object ("Here is the JSON you requested:")
- It produces valid JSON that doesn't match your expected shape (wrong types, missing keys, extra commentary keys)
- Nested objects or arrays get truncated if the response is cut off by
max_tokens
None of these are bugs — they're the natural behavior of a model that's been trained to be conversational. If you're building anything that parses the response programmatically, you need a stricter contract.
Using tool definitions to force schema-conformant output
The most reliable way to get structured output from Claude is to define a tool whose input_schema is exactly the JSON structure you want, and then require Claude to use it. Claude was trained extensively on tool use, so when you define a schema this way, the model treats filling it out as a first-class task rather than an afterthought.
{
"name": "extract_contact",
"description": "Extract contact information from the input text",
"input_schema": {
"type": "object",
"properties": {
"name": { "type": "string" },
"email": { "type": "string" },
"age": { "type": "integer" }
},
"required": ["name", "email"]
}
}
You then pass this as a tool in the request and set tool_choice to force that specific tool, rather than letting the model decide whether to call it. When Claude responds, the arguments come back as a structured tool_use block instead of free text — no markdown fences, no preamble, no parsing guesswork.
If you're calling Claude through SubToAPI, this works exactly the same way against the /v1/messages endpoint, since tool calling is passed through unchanged. A minimal request looks like this:
curl https://api.subtoapi.app/v1/messages \
-H "Authorization: Bearer $SUBTOAPI_KEY" \
-H "content-type: application/json" \
-d '{
"model": "claude-sonnet-4",
"max_tokens": 1024,
"tools": [{
"name": "extract_contact",
"description": "Extract contact information from the input text",
"input_schema": {
"type": "object",
"properties": {
"name": { "type": "string" },
"email": { "type": "string" },
"age": { "type": "integer" }
},
"required": ["name", "email"]
}
}],
"tool_choice": { "type": "tool", "name": "extract_contact" },
"messages": [
{ "role": "user", "content": "John Doe, 34, reachable at john@example.com" }
]
}'
The response contains a tool_use content block with input already parsed as a JSON object matching your schema — no string parsing required on your end. See /docs/tools for the full tool-use reference and /docs/messages for request/response shapes.
When plain prompting is good enough
Forcing a tool call adds a bit of overhead to your request structure, so for low-stakes use cases — a quick script, an internal tool, a one-off data transform — plain prompting with explicit formatting instructions is often fine:
Respond with only a single JSON object, no other text, no markdown formatting.
Schema: {"summary": string, "sentiment": "positive"|"neutral"|"negative", "keywords": string[]}
Pair this with a low temperature and always parse defensively:
function parseClaudeJson(text) {
const cleaned = text.trim().replace(/^```json\n?|```$/g, "");
return JSON.parse(cleaned);
}
This gets you 90% of the way there for non-critical paths, but it's not something you should rely on for anything that feeds a database, a billing pipeline, or a user-facing UI where a malformed response causes a visible error.
Handling arrays, nested objects, and enums
Schema design matters more than most people expect. A few practical rules:
- Use
enumfor fixed value sets (status codes, categories) instead of free-text strings — this dramatically reduces variance in output. - Keep nesting shallow where possible. Deeply nested schemas (4+ levels) increase the chance of a missing or malformed field.
- Mark fields
requiredexplicitly. Claude respectsrequiredin the schema and will populate those fields even for sparse input, sometimes inferring reasonable defaults — which you should validate, not trust blindly. - Validate on your side anyway. A tool schema constrains the shape of the output, but it doesn't guarantee semantic correctness (e.g., a plausible-looking but wrong email). Always run the result through a validator like
zodorajvbefore using it downstream.
Streaming structured output
If you're streaming responses and also need structured JSON, tool use blocks stream as partial JSON deltas that you accumulate and parse once the block is complete — you generally shouldn't try to JSON.parse mid-stream. See /docs/streaming for details on how partial tool inputs are delivered incrementally.
Getting started
If you already have Claude access through a subscription and want a straightforward HTTPS API to send these requests from a backend, /docs/quickstart walks through generating a sub_live_... key and making your first request. Plans start at €9/month on the Solo tier, with team seats available on the Team and Scale plans — see /pricing for details, or start a free trial at /signup.
Questions
Does Claude have a dedicated JSON mode like some other model APIs? No. Claude doesn't have a response_format: json flag. The equivalent, and more reliable, approach is defining a tool with an input_schema and forcing that tool call — Claude returns structured input matching your schema.
Can I validate that Claude's JSON output matches my schema before using it? Yes, and you should. Tool schemas constrain shape reliably, but running the result through zod, ajv, or a similar validator catches semantic issues (wrong formats, out-of-range values) the schema alone can't enforce.
What happens if the JSON gets cut off by max_tokens? Truncated tool inputs produce invalid JSON when parsed. Set max_tokens generously for the expected output size, and check the stop_reason in the response — if it's max_tokens rather than tool_use, treat the output as incomplete.