← Blog

Claude API Structured Output with JSON Schema

2026-09-24 · 4 min read · SubToAPI Team

Claude doesn't have a dedicated "JSON mode" toggle like some other model APIs. Instead, the reliable way to get structured, schema-conformant JSON out of the Claude API is to define a tool with an input schema and force Claude to call it using tool_choice. This guide covers exactly how to set that up, plus fallback patterns for when you can't use tools.

If you just want valid JSON back from a prompt, the short answer is: don't ask nicely in plain text and hope — define a JSON schema as a tool, force that tool with tool_choice, and parse tool_use.input directly. That input is already parsed JSON matching your schema, not a string you need to regex out of a response.

Why "just ask for JSON" doesn't work reliably

If you prompt Claude with "respond only in JSON" and no schema enforcement, you'll usually get JSON — but you'll also occasionally get:

None of this is a "bug" — it's a text completion model doing text completion. Tool use bypasses this because it constrains generation to a structured schema and returns the result as a distinct content block, not free text you need to strip.

The tool-based approach

Define your desired output shape as a JSON Schema inside a tool definition, then force Claude to call it with tool_choice.

{
  "model": "claude-sonnet-4-5",
  "max_tokens": 1024,
  "tools": [
    {
      "name": "extract_invoice",
      "description": "Extract structured invoice data from the provided text.",
      "input_schema": {
        "type": "object",
        "properties": {
          "vendor": { "type": "string" },
          "invoice_number": { "type": "string" },
          "total_amount": { "type": "number" },
          "currency": { "type": "string" },
          "line_items": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "description": { "type": "string" },
                "quantity": { "type": "number" },
                "unit_price": { "type": "number" }
              },
              "required": ["description", "quantity", "unit_price"]
            }
          }
        },
        "required": ["vendor", "invoice_number", "total_amount", "currency"]
      }
    }
  ],
  "tool_choice": { "type": "tool", "name": "extract_invoice" },
  "messages": [
    { "role": "user", "content": "Invoice from Acme Corp #INV-2291, total $450.00 USD, 3 units of widgets at $150 each." }
  ]
}

With tool_choice forced to a specific tool, Claude has to respond with a tool call matching that schema — it can't just answer in prose. The response contains a tool_use content block:

{
  "type": "tool_use",
  "id": "toolu_01A...",
  "name": "extract_invoice",
  "input": {
    "vendor": "Acme Corp",
    "invoice_number": "INV-2291",
    "total_amount": 450.0,
    "currency": "USD",
    "line_items": [
      { "description": "widgets", "quantity": 3, "unit_price": 150 }
    ]
  }
}

input is already a parsed object — no string splitting, no fence stripping, no JSON.parse failing on trailing commentary.

Building the request in JavaScript

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: 1024,
    tools: [{
      name: "classify_ticket",
      description: "Classify a support ticket into a structured category.",
      input_schema: {
        type: "object",
        properties: {
          category: { type: "string", enum: ["billing", "bug", "feature_request", "other"] },
          priority: { type: "string", enum: ["low", "medium", "high", "urgent"] },
          summary: { type: "string" }
        },
        required: ["category", "priority", "summary"]
      }
    }],
    tool_choice: { type: "tool", name: "classify_ticket" },
    messages: [{ role: "user", content: ticketText }]
  })
});

const data = await response.json();
const structured = data.content.find(b => b.type === "tool_use").input;

Because input_schema uses standard JSON Schema keywords — enum, required, nested object/array types — you can generate it programmatically from Zod, Pydantic, or any schema library you already use, instead of hand-writing it.

Validate anyway

Tool schemas make malformed JSON far less likely, but they don't guarantee semantic correctness — a model can still put a plausible-but-wrong string in an enum field in edge cases, or omit an optional field you actually needed. Run the returned object through the same validator you used to generate the schema (Zod's .parse(), Pydantic's model validation, Ajv) before it hits your database or downstream logic. Treat the tool schema as a strong prior on shape, not a hard contract.

When you can't use tools

Some workflows — long free-form generation that includes an embedded JSON block, for instance — don't fit the tool-call pattern. In those cases:

This works but is strictly less reliable than tool-forced output, so use it only when the tool pattern genuinely doesn't fit your use case.

Using this through SubToAPI

If your team is calling Claude through SubToAPI instead of managing raw API credentials, the request and response shapes above are unchanged — you're hitting /v1/messages on api.subtoapi.app with a sub_live_... key instead of Anthropic's endpoint directly. Tool definitions, tool_choice, and structured tool_use blocks behave the same way. See the tools docs and messages reference for full parameter details, or the quickstart if you're setting up your first key.

FAQ

Does Claude have a dedicated JSON mode like response_format: json_object? No. The equivalent, more powerful pattern is forcing a tool call with tool_choice, which constrains output to a schema and returns it as a parsed object in a tool_use block.

Can I nest objects and arrays in the input schema? Yes. input_schema follows standard JSON Schema, so nested objects, arrays of objects, enums, and required fields all work the same way they would in Ajv or any other JSON Schema validator.

Should I still validate the output if I used a tool schema? Yes. Tool schemas drastically reduce malformed output but don't guarantee semantic correctness, so run the response through your existing Zod/Pydantic/Ajv validator before using it downstream.

Turn your Claude access into an HTTPS API

SubToAPI gives you application API keys, streaming, tool use and usage insights on top of your existing Claude access — set up in minutes.

Start free  Read the quickstart →