Claude API JSON Schema Validation Tutorial
Claude API JSON Schema Validation Tutorial
If you're building anything that feeds Claude's output into another system — a database, a form, another API call — you need the response to come back as valid, predictable JSON. The short answer: Claude doesn't have a dedicated "JSON mode" flag like some other providers, but you can get reliable schema-validated output by using tool use (function calling) with an input_schema, combined with runtime validation on your side using a library like zod or ajv.
This tutorial covers both halves of the problem: getting Claude to produce JSON that matches a schema, and verifying that it actually does before you trust it downstream. Skipping the second step is the most common mistake — even well-prompted models occasionally drift from the schema, so validation isn't optional if you're shipping to production.
Why tool use is the right approach
You could ask Claude to "respond only in JSON matching this structure" in a system prompt, and it will usually comply. But "usually" isn't good enough for pipelines. Claude's tool use feature forces the model to generate arguments that conform to a JSON schema you define, because the schema is passed as part of the tool definition rather than as a loose instruction. The model treats it as a structured function call, not free text.
Here's a minimal example using the Claude Messages API directly:
curl https://api.anthropic.com/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d '{
"model": "claude-3-5-sonnet-20241022",
"max_tokens": 512,
"tools": [
{
"name": "extract_invoice",
"description": "Extract structured invoice data",
"input_schema": {
"type": "object",
"properties": {
"invoice_number": { "type": "string" },
"total_amount": { "type": "number" },
"currency": { "type": "string" },
"line_items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"description": { "type": "string" },
"amount": { "type": "number" }
},
"required": ["description", "amount"]
}
}
},
"required": ["invoice_number", "total_amount", "currency"]
}
}
],
"tool_choice": { "type": "tool", "name": "extract_invoice" },
"messages": [
{ "role": "user", "content": "Invoice #4821, total $1,250.00 USD, one line item: consulting services $1,250.00" }
]
}'
By setting tool_choice to force the specific tool, you guarantee the response will contain a tool_use block with input that matches your schema's shape — Claude is effectively constrained to fill in the fields you defined.
Parsing the response
The JSON lives inside the content array, in a block with type: "tool_use":
{
"content": [
{
"type": "tool_use",
"name": "extract_invoice",
"input": {
"invoice_number": "4821",
"total_amount": 1250.00,
"currency": "USD",
"line_items": [
{ "description": "consulting services", "amount": 1250.00 }
]
}
}
]
}
That input object is your validated-ish JSON. It should match the schema, but you still need to check it programmatically.
Validating with a runtime schema library
Even with tool use, don't skip explicit validation. Models can occasionally omit a required field, return a string where a number was expected, or hallucinate a slightly different structure under edge-case inputs. Use zod (JavaScript/TypeScript) or ajv (plain JSON Schema) to catch this before it hits your database.
import { z } from "zod";
const InvoiceSchema = z.object({
invoice_number: z.string(),
total_amount: z.number(),
currency: z.string(),
line_items: z.array(
z.object({
description: z.string(),
amount: z.number(),
})
),
});
const toolUseBlock = response.content.find(b => b.type === "tool_use");
const result = InvoiceSchema.safeParse(toolUseBlock.input);
if (!result.success) {
console.error("Schema validation failed:", result.error.format());
// retry, fall back, or flag for review
} else {
console.log("Valid invoice:", result.data);
}
If validation fails, the pragmatic options are: retry the request with a stricter system prompt reminding Claude of the constraint, lower the temperature, or send the validation errors back to Claude in a follow-up message and ask it to correct its own output.
Handling nested and conditional schemas
Tool use supports the full JSON Schema spec for input_schema, including enum, oneOf, nested objects, and arrays of objects. Keep schemas as flat as reasonably possible — deeply nested or highly conditional schemas increase the chance of malformed output, even with tool forcing. If you need conditional logic (field A required only when field B equals X), it's usually more reliable to split that into two separate tool definitions than to express it as a single oneOf schema.
Using this through SubToAPI
If your app already calls Claude through SubToAPI, the tool use flow is identical — you send the same tools array and input_schema to the SubToAPI endpoint using your sub_live_... key instead of an Anthropic key:
curl https://api.subtoapi.app/v1/messages \
-H "Authorization: Bearer $SUBTOAPI_KEY" \
-H "content-type: application/json" \
-d '{
"model": "claude-3-5-sonnet-20241022",
"max_tokens": 512,
"tools": [ /* same tool definition as above */ ],
"tool_choice": { "type": "tool", "name": "extract_invoice" },
"messages": [ { "role": "user", "content": "..." } ]
}'
This is useful if several services or team members need to call Claude with schema-validated outputs but you want centralized key management, usage metadata per app, and seat-based access instead of sharing a raw provider key. See the tool use docs and messages API reference for the full parameter list, or the quickstart if you're setting this up for the first time.
Testing your schema before production
Before wiring schema validation into a live pipeline, run a batch of representative inputs (including edge cases — empty strings, missing fields in the source text, ambiguous numbers) through your prompt and check the failure rate. A 2–5% validation failure rate is normal and should be handled with a retry-with-correction loop rather than treated as a bug. If failures spike above that, the schema is usually too complex or the tool description too vague — tightening the description field on both the tool and individual properties often fixes it without touching the prompt at all.
questions
Does Claude have a built-in "JSON mode" like some other APIs? No. Claude doesn't expose a dedicated JSON-mode flag. The reliable equivalent is tool use with a forced tool_choice, which constrains the model's output to match your input_schema.
Can I validate Claude's output without using tool use? Yes, but it's less reliable. You can prompt Claude to return JSON in a system message and parse response.content[0].text, but you'll see more formatting drift (markdown code fences, trailing commentary) than with tool use.
What should I do when validation fails repeatedly? Simplify the schema, tighten field descriptions, and consider a correction loop: send the validation error back to Claude as a follow-up message and ask it to fix the specific field rather than regenerating the whole response.