← Blog

Claude API Structured Data Extraction with Schemas

2026-11-08 · 5 min read · SubToAPI Team

Structured data extraction with the Claude API means turning unstructured text — emails, PDFs, scraped web pages, chat logs — into predictable JSON that your application can store, query, or pass to another system. The most reliable way to do this is to define a schema (via tool use / function calling or a strict prompt template) that tells Claude exactly which fields to return, what types they should be, and what to do when a field is missing.

This article walks through the practical patterns: when to use tool-based extraction vs. plain JSON prompting, how to design a schema that minimizes hallucinated fields, how to validate the output, and how to handle edge cases like partial matches or ambiguous data. The examples use the Claude API directly, with notes on where a proxy layer like SubToAPI simplifies the operational side (auth, streaming, usage tracking) once you're running extraction at scale.

Why Schema-Based Extraction Beats Free-Form Prompting

Asking Claude to "pull out the key details" from a document works for demos but breaks in production. Field names drift between calls, nested objects get flattened inconsistently, and optional fields sometimes disappear entirely. A schema fixes this by giving the model a contract to fill in rather than a vague instruction to interpret.

There are two common approaches:

  1. Tool use (function calling) — define a JSON schema as a tool, and Claude returns a tool_use block matching that schema. This is the most reliable method because the model is constrained to the tool's input schema.
  2. JSON-mode prompting — instruct Claude to respond only with JSON matching a schema you paste into the prompt, then parse and validate the output yourself. Works well for simpler extractions where you don't need the tool-call wrapper.

For anything you'll run repeatedly in production, tool use is worth the extra setup.

Defining the Extraction Schema

Start with a JSON Schema object describing the exact shape you need. Keep it flat where possible — nested structures increase the chance of malformed output.

{
  "name": "extract_invoice",
  "description": "Extract structured fields from an invoice document",
  "input_schema": {
    "type": "object",
    "properties": {
      "invoice_number": { "type": "string" },
      "invoice_date": { "type": "string", "format": "date" },
      "vendor_name": { "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": ["invoice_number", "total_amount", "currency"]
  }
}

A few design rules that reduce errors:

Calling the API with the Tool Defined

const response = await fetch("https://api.anthropic.com/v1/messages", {
  method: "POST",
  headers: {
    "x-api-key": process.env.ANTHROPIC_API_KEY,
    "anthropic-version": "2023-06-01",
    "content-type": "application/json",
  },
  body: JSON.stringify({
    model: "claude-sonnet-4-5",
    max_tokens: 1024,
    tools: [invoiceSchema],
    tool_choice: { type: "tool", name: "extract_invoice" },
    messages: [
      { role: "user", content: `Extract the invoice fields from this document:\n\n${documentText}` }
    ]
  })
});

Forcing tool_choice to the specific tool name guarantees Claude returns a tool call rather than a plain-text reply, which simplifies downstream parsing — you always get a tool_use block with structured input matching your schema.

If you're running this through SubToAPI instead of calling Anthropic directly, the request shape is the same — point it at https://api.subtoapi.app/v1/messages with Authorization: Bearer $SUBTOAPI_KEY, and you get per-application API keys, usage metadata per extraction call, and streaming support without changing your schema logic. See /docs/tools for the full tool-use reference and /docs/messages for the base request format.

Validating and Handling Partial Extractions

Even with a tight schema, you should still validate the output before trusting it downstream — models occasionally return a type mismatch (string instead of number) or skip a required field when the source text genuinely doesn't contain it.

import Ajv from "ajv";
const ajv = new Ajv();
const validate = ajv.compile(invoiceSchema.input_schema);

const toolUse = response.content.find(b => b.type === "tool_use");
const valid = validate(toolUse.input);

if (!valid) {
  // log validate.errors, flag for manual review, or retry with a clarifying prompt
}

For documents where some fields genuinely aren't present (a receipt with no invoice number, for example), it helps to add an explicit instruction in the system prompt: "If a field is not present in the text, omit it rather than guessing." This single sentence meaningfully reduces hallucinated values in optional fields.

Batching and Scaling Extraction Jobs

Once the schema is validated on a handful of documents, the bottleneck shifts to throughput and reliability — retrying failed calls, tracking token usage per document type, and keeping API keys scoped per environment or team. If you're extracting from thousands of documents a day, running this through a managed layer rather than raw Anthropic keys gives you separate sub_live_... keys per app or environment, streaming for long documents, and usage metadata per request so you can see cost per extraction type. Check /pricing for plan details and /docs/quickstart to get a key issued in a few minutes.

Keeping Schemas Maintainable

As your extraction needs grow, resist the urge to build one giant schema with every possible field. Split schemas by document type (invoices, resumes, support tickets) and version them — when you add a field, bump a version string in your own metadata so you can trace which schema version produced which stored record. This matters more than it seems once you're backfilling or re-running extraction after a schema change.

Questions

Does Claude guarantee valid JSON output for extraction? Tool use with a forced tool_choice reliably returns structured input matching your schema's types, but you should still run schema validation (e.g. with Ajv) before writing results to a database, since edge cases like malformed dates can still occur.

Should I use tool use or just ask for JSON in the prompt? Tool use is more reliable for production because the model is constrained to your schema; plain JSON prompting is fine for quick scripts or simple one-off extractions where you'll eyeball the output.

How do I handle documents where required fields are missing? Make only genuinely universal fields required in the schema, instruct the model explicitly to omit rather than guess missing optional fields, and flag any validation failures for manual review instead of silently accepting malformed data.

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 →