Claude API Structured Output: JSON Schema and Strict Tool Use

March 25, 2026 · 6 min read · claude-api, structured-output, json, tool-use
Claude API Structured Output: JSON Schema and Strict Tool Use

For a JSON answer from Claude, use output_config.format with type: “json_schema”. For arguments to a tool your application will execute, use strict: true on that tool. Both constrain the supported JSON Schema shape. Neither verifies that an extracted fact is true, so validate the result before it enters a workflow.

That is the current answer to “how do I get clean JSON from Claude with no extra text?” The old answer on this page was to force a tool call or start the assistant response with {. Anthropic has since shipped native structured outputs. More critically, assistant prefill returns HTTP 400 on Claude 4.6 and later. I have updated the examples below to the current Messages API; the Anthropic structured outputs documentation is the source for the API behavior.

Choose the output contract first

NeedClaude API featureWhere the object arrives
A JSON answer for extraction or classificationoutput_config.formatText content block
Valid arguments for a function you executeTool definition with strict: truetool_use.input
Free-form explanationNeitherText content block

You can combine JSON outputs and strict tools in one request when an agent both calls tools and must finish with a typed answer. If the job is just “extract a contact from this email,” native JSON output is the shorter path. A forced tool still makes sense when the result represents an actual action your application will handle.

Native Claude structured output: JSON Schema in TypeScript

This example extracts four fields from an email. The API schema covers the supported shape; Zod checks constraints the API schema does not enforce, such as email syntax and the priority range.

import Anthropic from "@anthropic-ai/sdk";
import { z } from "zod";

const client = new Anthropic();

const Contact = z.object({
  name: z.string(),
  email: z.string().email(),
  intent: z.enum(["sales", "support", "spam", "other"]),
  priority: z.number().int().min(1).max(5),
});

const contactJsonSchema = {
  type: "object" as const,
  properties: {
    name: { type: "string" as const },
    email: { type: "string" as const },
    intent: {
      type: "string" as const,
      enum: ["sales", "support", "spam", "other"],
    },
    priority: { type: "integer" as const },
  },
  required: ["name", "email", "intent", "priority"],
  additionalProperties: false,
};

async function extractContact(emailBody: string) {
  const response = await client.messages.create({
    model: "claude-sonnet-4-6",
    max_tokens: 512,
    messages: [{
      role: "user",
      content: "Extract the contact from this email: " + emailBody,
    }],
    output_config: {
      format: {
        type: "json_schema",
        schema: contactJsonSchema,
      },
    },
  });

  if (response.stop_reason !== "end_turn") {
    throw new Error("Claude did not complete a JSON response");
  }
  const block = response.content.find((item) => item.type === "text");
  if (!block || block.type !== "text") {
    throw new Error("Claude returned no JSON text block");
  }
  return Contact.parse(JSON.parse(block.text));
}

A completed structured response gives you JSON in the text block. Check the stop reason because a refusal or a response cut off at max_tokens can fall outside the schema. The Zod parse catches business-rule failures and gives downstream code a typed object. It does not prove the name or address was present in the original email; verify source evidence separately when that matters.

Strict tool use: when input_schema is the contract

A Claude tool’s input_schema tells the model which arguments your function accepts. tool_choice can require a call to a named tool. Add strict: true when those arguments must follow the supported schema exactly.

const response = await client.messages.create({
  model: "claude-sonnet-4-6",
  max_tokens: 512,
  messages: [{
    role: "user",
    content: "Classify this email: " + emailBody,
  }],
  tools: [{
    name: "record_contact",
    description: "Return the extracted contact for review.",
    input_schema: contactJsonSchema,
    strict: true,
  }],
  tool_choice: { type: "tool", name: "record_contact" },
});

const call = response.content.find((item) => item.type === "tool_use");
if (!call || call.type !== "tool_use") {
  throw new Error("Claude did not produce the required tool call");
}
const contact = Contact.parse(call.input);

The tool call is a request, not an executed side effect. Your code still decides whether to call a CRM, write a file, or do nothing. For the full stop_reason: “tool_use” loop and tool errors, see the Claude API tool use guide.

Claude JSON Schema and input_schema restrictions

The query “Claude API tool use input_schema Draft 2020-12 restrictions” points to an easy mistake: the API accepts a subset of JSON Schema for constrained outputs. You cannot assume every validation keyword in a local schema will be enforced by the decoder.

  • Use supported basic types, enum, const, required, and additionalProperties: false.
  • Do not put numeric bounds such as minimum or maximum, or string lengths such as minLength, in a raw constrained schema. The API rejects unsupported features with a 400.
  • Keep email format, ranges, cross-field rules, and source checks in your application’s validator. SDK helpers can transform some unsupported constraints and validate after generation.
  • Keep schemas reasonably small. Anthropic compiles them and caches the grammar; the first call for a new structure can be slower.

The official JSON Schema limits list the currently supported subset. If you are porting OpenAI’s strict response_format, map it to output_config.format for a final JSON answer and to strict tool use for function arguments. The OpenAI-to-Claude migration guide covers the surrounding SDK differences.

What happened to assistant prefill?

Older Claude examples ended the request with an assistant message containing { and prepended that brace before parsing. That technique shaped text; it never enforced a schema. Claude 4.6 and later reject assistant prefill, so the old example here would fail on its own model ID.

Replace prefill with native JSON outputs for extraction. Replace a prefilled function-like response with strict tool use when code will act on the arguments. Prompt-only “return JSON” remains useful for a quick experiment, but it offers no parseability or schema guarantee.

Production checks beyond the schema

For each response I check completion, parse into an application type, and record enough source context to inspect a bad extraction. A valid enum can still be the wrong classification. If the model is allowed to trigger a side effect, place approval and idempotency checks after parsing and before the write.

For repeated schemas, watch input token usage and caching. The schema and tool definitions have a cost, and changing the schema can invalidate cached grammar. The Claude API prompt caching guide covers the larger cost picture.

The practical rule: use JSON outputs for a typed answer, strict tools for typed actions, and local validation for facts and business constraints.

The context layer for your AI agents

Your agents answer from whatever the retriever finds, and too often that is last quarter's truth. I build the context layer they answer and act from: a temporal knowledge graph that keeps every fact with its source and the time it held, reads with each person's own permissions, and writes nothing without a person's approval. On your own tenant, billed by the hour, step by step.

Scope my automation in 24h

Two fields. I reply within 24h with a written scope: either “yes, about X hours over Y weeks” or “no, here’s why not”.

See what you get first: sample scope →

Your details are used only to answer this request — no sharing, no newsletter. Privacy

Not ready to write it up? Book a 30-min call instead →
✓

Request received

You’ll hear from me within 24h with an honest assessment.

Prefer to talk? 30-min roadmap call →