Claude API Function Calling for Reliable JSON Output
If you need the Claude API to return structured, predictable JSON instead of free-form prose, the answer is tool use (Claude's version of function calling). You define a JSON schema for the data you want back, pass it to the API as a "tool," and Claude responds with a tool_use block containing arguments that match your schema — not a paragraph you have to parse with regex.
This is the pattern to use any time you're extracting data, filling a form, classifying content, or feeding Claude's output into another system. Prompting Claude to "respond only in JSON" works most of the time, but it's fragile: markdown fences, stray commentary, or a missing comma can break your parser. Tool use removes that ambiguity because the model is constrained to produce arguments that conform to a schema you control.
Why prompting for JSON isn't enough
A plain prompt like "return the result as JSON" relies on the model following instructions perfectly, every time, across every input. In practice you'll see:
- Extra text before or after the JSON block ("Here's the JSON you requested:")
- Markdown code fences wrapping the object
- Inconsistent key names or missing fields
- Invalid JSON on edge cases (trailing commas, unescaped quotes)
Tool use fixes this at the API level. You describe the exact shape of the output as a JSON Schema, Claude calls the "tool" with arguments matching that shape, and you parse tool_use.input directly — no string cleanup required.
Defining a tool for structured output
A tool definition has a name, a description, and an input_schema using standard JSON Schema. Here's a tool for extracting structured data from a support ticket:
{
"name": "extract_ticket_info",
"description": "Extract structured fields from a customer support message",
"input_schema": {
"type": "object",
"properties": {
"summary": { "type": "string" },
"category": {
"type": "string",
"enum": ["billing", "bug", "feature_request", "account", "other"]
},
"priority": {
"type": "string",
"enum": ["low", "medium", "high", "urgent"]
},
"requires_followup": { "type": "boolean" }
},
"required": ["summary", "category", "priority", "requires_followup"]
}
}
By listing fields in required and using enum for closed sets of values, you narrow the model's choices and make downstream parsing deterministic.
Example: calling the API
Here's the full request/response cycle using curl. SubToAPI exposes the same Messages and tool-use interface as the native Claude API, so this works whether you're calling Claude directly or routing through a SubToAPI key:
curl https://api.subtoapi.app/v1/messages \
-H "Authorization: Bearer $SUBTOAPI_KEY" \
-H "content-type: application/json" \
-d '{
"model": "claude-sonnet-4",
"max_tokens": 512,
"tools": [{
"name": "extract_ticket_info",
"description": "Extract structured fields from a customer support message",
"input_schema": {
"type": "object",
"properties": {
"summary": { "type": "string" },
"category": { "type": "string", "enum": ["billing", "bug", "feature_request", "account", "other"] },
"priority": { "type": "string", "enum": ["low", "medium", "high", "urgent"] },
"requires_followup": { "type": "boolean" }
},
"required": ["summary", "category", "priority", "requires_followup"]
}
}],
"tool_choice": { "type": "tool", "name": "extract_ticket_info" },
"messages": [{
"role": "user",
"content": "My card was charged twice this month and I need a refund ASAP."
}]
}'
Setting tool_choice to force a specific tool is the key detail here — it tells Claude it must call extract_ticket_info rather than just answering in plain text. The response contains a tool_use content block:
{
"type": "tool_use",
"name": "extract_ticket_info",
"input": {
"summary": "Customer charged twice this month, requesting a refund",
"category": "billing",
"priority": "high",
"requires_followup": true
}
}
That input object is already valid JSON matching your schema — no parsing tricks needed.
Parsing the response in JavaScript
const res = 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",
max_tokens: 512,
tools: [ticketTool],
tool_choice: { type: "tool", name: "extract_ticket_info" },
messages: [{ role: "user", content: ticketText }],
}),
});
const data = await res.json();
const toolCall = data.content.find((c) => c.type === "tool_use");
const ticket = toolCall.input; // typed object, ready to use
No JSON.parse on model text, no try/catch for malformed output — the structure is enforced by the schema contract.
Tips for reliable structured extraction
- Keep schemas flat where possible. Deeply nested objects increase the chance of missing or malformed fields.
- Use
enumaggressively for category-like fields instead of free-text strings. - Mark every field you need as
required. Claude generally fills required fields reliably; optional fields are more likely to be omitted. - Give the tool a clear, specific description. The model uses it to decide what data matters, especially when you have multiple tools defined.
- Force the tool with
tool_choicewhen you always want structured output — otherwise Claude may choose to respond in plain text instead of calling the tool.
If you're running multiple extraction tools or chaining tool calls together, the deeper mechanics of parallel and sequential tool use are worth understanding — see /docs/tools for the full reference. For general request/response structure, /docs/messages covers the Messages API shape, and /docs/quickstart walks through authentication and your first call end to end.
Questions
Does tool use guarantee perfectly valid JSON every time? It guarantees output conforming to your JSON Schema's structure far more reliably than prompt-based instructions, but you should still validate the response against your schema before using it in production, especially for complex nested objects.
Can I use function calling just for JSON output without actually calling a function? Yes — this is a common pattern. You define a tool purely to describe the shape of data you want, set tool_choice to force that tool, and never actually execute anything. The "function" is really just a schema contract.
What's the difference between tool use and Claude's native JSON mode-style prompting? Prompt-based JSON relies on instruction-following and can produce malformed or decorated output. Tool use enforces a schema at the API level, returning a parsed tool_use.input object instead of a text string you need to clean and parse yourself.