Claude API Structured Output Example (Tool Use Method)
If you need Claude to return data in a predictable shape — a JSON object with specific fields, an array of records, a nested object matching your database schema — you want structured output. Claude doesn't have a dedicated "JSON mode" toggle like some other APIs. Instead, you get structured output by defining a tool with an input schema and forcing Claude to call it. This article shows exactly how that works, with a full working example.
The core idea: you describe the shape of the data you want as a JSON Schema, attach it to a tool definition, send it with your message, and tell Claude it must use that tool. Claude's response then contains a tool_use block whose input field is already a parsed object matching your schema — no regex, no markdown-fence stripping, no "please respond only in JSON" prompt begging.
Why tool use is the structured output method
Early approaches to getting JSON out of LLMs relied on prompting: "respond only with valid JSON, no explanation." This works most of the time but fails unpredictably — the model adds a preamble, wraps the JSON in a code block, or produces invalid syntax under load. Tool use solves this at the API level because the schema is enforced as part of the tool-calling mechanism, not just a suggestion in the prompt.
The tradeoff: you're asking Claude to call a "tool" that doesn't actually do anything — it just packages your data. This is a well-established pattern and works reliably across Claude models.
A full structured output example
Suppose you're extracting structured data from a support ticket: customer name, issue category, urgency, and a short summary.
{
"name": "extract_ticket",
"description": "Extract structured fields from a support ticket",
"input_schema": {
"type": "object",
"properties": {
"customer_name": { "type": "string" },
"category": {
"type": "string",
"enum": ["billing", "technical", "account", "other"]
},
"urgency": {
"type": "string",
"enum": ["low", "medium", "high"]
},
"summary": { "type": "string" }
},
"required": ["customer_name", "category", "urgency", "summary"]
}
}
Here's the full request using curl against SubToAPI's Claude-compatible endpoint:
curl https://api.subtoapi.app/v1/messages \
-H "Authorization: Bearer $SUBTOAPI_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "claude-sonnet-4-5",
"max_tokens": 500,
"tools": [{
"name": "extract_ticket",
"description": "Extract structured fields from a support ticket",
"input_schema": {
"type": "object",
"properties": {
"customer_name": { "type": "string" },
"category": { "type": "string", "enum": ["billing", "technical", "account", "other"] },
"urgency": { "type": "string", "enum": ["low", "medium", "high"] },
"summary": { "type": "string" }
},
"required": ["customer_name", "category", "urgency", "summary"]
}
}],
"tool_choice": { "type": "tool", "name": "extract_ticket" },
"messages": [{
"role": "user",
"content": "Ticket from Maria Gomez: I was charged twice for my subscription this month and need a refund ASAP, this is affecting my business."
}]
}'
The tool_choice field with "type": "tool" forces Claude to call exactly that tool, which is what guarantees structured output instead of a free-text reply.
Parsing the response
The response contains a tool_use block in the content array:
{
"content": [
{
"type": "tool_use",
"id": "toolu_01abc",
"name": "extract_ticket",
"input": {
"customer_name": "Maria Gomez",
"category": "billing",
"urgency": "high",
"summary": "Double-charged for subscription, requesting refund."
}
}
]
}
In JavaScript, extracting the structured data is one line:
const response = await fetch("https://api.subtoapi.app/v1/messages", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.SUBTOAPI_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
model: "claude-sonnet-4-5",
max_tokens: 500,
tools: [/* schema from above */],
tool_choice: { type: "tool", name: "extract_ticket" },
messages: [{ role: "user", content: ticketText }]
})
});
const data = await response.json();
const toolUse = data.content.find(block => block.type === "tool_use");
const ticket = toolUse.input; // already a parsed object
No JSON.parse() call needed on free text, no stripping of markdown code fences. The input field is structured data by construction.
Handling nested and array outputs
The same pattern scales to more complex shapes. For an array of extracted entities:
"input_schema": {
"type": "object",
"properties": {
"entities": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": { "type": "string" },
"type": { "type": "string", "enum": ["person", "organization", "location"] }
},
"required": ["name", "type"]
}
}
},
"required": ["entities"]
}
Wrap arrays in a top-level object (as shown) rather than making the schema's root type array — tool input schemas expect an object at the top level, so a wrapper key like entities or items is the standard workaround.
Common pitfalls
- Vague field descriptions. Add a
descriptionto each property if the field name alone is ambiguous — it meaningfully improves accuracy, especially for enums and dates. - Missing
requiredarrays. Withoutrequired, Claude may omit fields it's unsure about rather than guessing. - Multiple tools defined but no
tool_choice. If you give Claude several tools without forcing one, it may choose not to call any of them and just respond in text. Always settool_choiceexplicitly when structured output is mandatory. - Overly deep nesting. Schemas more than 3-4 levels deep sometimes reduce accuracy — flatten where you can.
If you're building this against SubToAPI, the request and response formats match the examples above exactly — see /docs/tools for the full tool-use reference and /docs/messages for general request shape. Streaming structured output (partial tool-input deltas) is covered in /docs/streaming.
FAQ
Does Claude have a native "JSON mode" like some other APIs? No. Structured output is achieved through tool use with a forced tool_choice, which is functionally equivalent and often more reliable because the schema is explicit.
What happens if the input doesn't match my schema? Claude is trained to respect the schema closely, but it's not a hard guarantee — always validate the returned object against your schema in code and handle mismatches with a retry or fallback.
Can I get multiple structured objects back in one response? Yes — define a schema with an array property (wrapped in a top-level object) to return a list of structured records in a single call, as shown in the entities example above.