Claude API Structured Output: Schema Validation Guide
When you need the Claude API to return data that matches a predictable shape — a JSON object with specific fields, correct types, and no missing keys — you're dealing with structured output and schema validation. The reliable way to do this with Claude is not to ask nicely in a prompt and hope for well-formed JSON back. It's to define a tool with a JSON Schema and force Claude to call it, then validate the result on your side before you trust it downstream.
This article covers the practical pattern: defining schemas, forcing tool use, handling validation failures, and where this fits when you're building on top of an API that proxies Claude, like SubToAPI.
Why prompting alone isn't schema validation
Asking Claude to "respond in JSON with fields x, y, z" works most of the time, but "most of the time" isn't good enough for a production pipeline. Models can add explanatory text before the JSON, wrap it in markdown code fences, drop a field, or produce a type mismatch (a string where you expected a number). None of that is a bug in Claude — it's the nature of free-form text generation.
Schema validation solves this at two layers:
- Generation-time constraint — using Claude's tool-use feature with a JSON Schema so the model is guided to produce arguments matching your schema.
- Runtime validation — checking the actual response against that schema before your application code touches it, because "guided" is not "guaranteed."
Defining a tool schema for structured output
The most reliable structured-output pattern with Claude is to define a single tool that represents the data you want, and force the model to use it via tool_choice. Here's a schema for extracting structured order data from unstructured text:
{
"name": "extract_order",
"description": "Extract order details from customer text",
"input_schema": {
"type": "object",
"properties": {
"customer_name": { "type": "string" },
"items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"sku": { "type": "string" },
"quantity": { "type": "integer" }
},
"required": ["sku", "quantity"]
}
},
"total_cents": { "type": "integer" },
"rush_delivery": { "type": "boolean" }
},
"required": ["customer_name", "items", "total_cents"]
}
}
When you send this tool and set tool_choice to force it, Claude's response contains a tool_use block with input matching that schema shape, instead of free-form prose you'd otherwise have to parse.
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-sonnet-4-5",
"max_tokens": 1024,
"tools": [{ "name": "extract_order", "input_schema": {...} }],
"tool_choice": {"type": "tool", "name": "extract_order"},
"messages": [{"role": "user", "content": "Jane Doe ordered 2x SKU-1001 and 1x SKU-2002, rush delivery, total $84.50"}]
}'
If you're calling Claude through SubToAPI, the request shape is the same — you send it to https://api.subtoapi.app/v1/messages with your sub_live_... key instead, which is useful if you want tool-based structured output without managing separate Anthropic billing per app. See /docs/tools for the tool-use reference and /docs/messages for the base request format.
Runtime validation: don't skip it
Forcing tool use dramatically reduces malformed output, but it doesn't eliminate edge cases — models can still return an empty array where you expected at least one item, or omit an optional field your downstream code assumes exists. Always validate the tool_use.input object against your schema with a real validator before using it.
import Ajv from "ajv";
const ajv = new Ajv();
const validate = ajv.compile(orderSchema);
const toolUse = response.content.find(b => b.type === "tool_use");
const valid = validate(toolUse.input);
if (!valid) {
console.error(validate.errors);
// retry, fall back, or flag for manual review
}
This is standard JSON Schema validation — the same Ajv or Zod setup you'd use for any API payload. The only Claude-specific part is where the object comes from.
Handling validation failures gracefully
Don't just throw on a failed validation. Build a retry path:
- Re-prompt with the error: send the validation errors back to Claude in a follow-up message ("your previous response was missing
total_cents, please retry") and let it self-correct. - Fallback schema: if strict extraction keeps failing, fall back to a looser schema with fewer required fields, then fill gaps with defaults.
- Log and sample: keep a sample of failures. If the same field fails repeatedly, your schema description is probably ambiguous — tighten the field description in the schema itself, since Claude reads those descriptions as instructions.
A common mistake is treating schema validation as a one-time integration task. In practice it's an ongoing feedback loop — you tighten schema descriptions based on real failure patterns from production traffic.
Streaming and structured output
If you're streaming responses, tool-use blocks still arrive as accumulated deltas — you typically buffer the input_json deltas and parse the complete JSON once the block closes, rather than validating partial JSON mid-stream. See /docs/streaming if you're combining this pattern with streaming responses through SubToAPI; the delta format is compatible with the standard Anthropic streaming events.
Multiple output types with one endpoint
If your application needs different structured shapes depending on context (e.g., extracting an order vs. extracting a support ticket), define multiple tools and let tool_choice: "auto" pick, or force a specific tool per request type. Each tool gets its own input_schema, and you validate each against its matching schema after the call.
Getting started
If you're already building on Claude and want a single API key per application instead of juggling Anthropic credentials across environments, SubToAPI gives you sub_live_... keys with usage metadata per key, which is useful once you have multiple structured-output pipelines running in parallel (extraction service, classification service, etc.) and want to see cost and volume broken out per integration rather than lumped into one account. Check /pricing for plan details or start with /docs/quickstart.
FAQ
Does Claude have a native "JSON mode" like some other APIs? Claude doesn't have a dedicated JSON-only response mode. The equivalent and more reliable approach is forcing tool use with a JSON Schema — it constrains the model's output shape more predictably than a plain "respond only in JSON" instruction.
Can I use Zod or Pydantic instead of raw JSON Schema? Yes. Zod (via zod-to-json-schema) and Pydantic both generate standard JSON Schema, which you pass directly as the tool's input_schema. This lets you validate with the same library you used to define the schema.
What happens if Claude's output doesn't match my schema at all? Forced tool use makes full mismatches rare, but always run runtime validation. On failure, re-prompt with the specific validation error rather than the original request — the model corrects far more reliably when told exactly what field or type was wrong.