Claude API Structured Data Extraction to JSON
Extracting Structured Data with the Claude API
If you need to pull structured fields — names, dates, amounts, categories, nested objects — out of unstructured text, emails, contracts, or scraped web pages, the Claude API can do this reliably when you give it a clear schema and the right prompting pattern. The two practical approaches are tool use with a JSON schema (the most reliable) and prompted JSON output with strict instructions (simpler, slightly less deterministic).
This article covers both, with working examples, validation strategies, and the common failure modes you'll hit when extraction runs at scale — missing fields, hallucinated values, and malformed JSON — plus how to fix them.
Why structured extraction is different from "just ask for JSON"
A lot of developers start by appending "respond only in JSON" to a prompt. That works for simple cases but breaks down once your schema has nested objects, optional fields, enums, or arrays of variable length. The model will sometimes add commentary before the JSON, wrap it in markdown fences, invent fields that don't exist in your schema, or silently drop a field it's unsure about.
The fix is to treat extraction as a tool call, not a chat response. You define a tool with a strict JSON schema, instruct the model to call that tool with the extracted data, and parse the tool_use block directly — no string parsing, no regex stripping of markdown fences.
Defining an extraction schema with tool use
Here's a schema for extracting structured invoice data from raw text:
{
"name": "extract_invoice",
"description": "Extract structured invoice data from text",
"input_schema": {
"type": "object",
"properties": {
"invoice_number": { "type": "string" },
"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", "vendor_name", "total_amount", "currency"]
}
}
With tool_choice forced to this tool, Claude has no path to return free text — it must populate this schema or explicitly refuse. That constraint is what makes extraction production-grade instead of best-effort.
Example request
curl https://api.subtoapi.app/v1/messages \
-H "Authorization: Bearer $SUBTOAPI_KEY" \
-H "content-type: application/json" \
-d '{
"model": "claude-sonnet-4-20250514",
"max_tokens": 1024,
"tools": [{
"name": "extract_invoice",
"description": "Extract structured invoice data from text",
"input_schema": {
"type": "object",
"properties": {
"invoice_number": {"type": "string"},
"vendor_name": {"type": "string"},
"total_amount": {"type": "number"},
"currency": {"type": "string"}
},
"required": ["invoice_number", "vendor_name", "total_amount", "currency"]
}
}],
"tool_choice": {"type": "tool", "name": "extract_invoice"},
"messages": [
{"role": "user", "content": "Invoice #INV-2024-0981 from Acme Supplies, total due $4,250.00 USD, payment terms net 30."}
]
}'
The response contains a tool_use content block with input already shaped exactly like your schema — ready to JSON.parse or map directly to a database row.
This pattern works through SubToAPI exactly as it does against Anthropic's native endpoint, since the request body and tool definitions are unchanged — see /docs/tools for the full reference and /docs/messages for message formatting basics.
Handling optional and missing fields
Real documents are messy. Not every invoice has a PO number; not every resume has a LinkedIn URL. Two strategies:
- Make fields optional in
requiredand instruct the model (indescriptionor a system prompt) to usenullrather than guessing. - Add a
confidencefield per extracted value if downstream logic needs to flag low-certainty extractions for human review.
A system prompt line like this helps a lot:
"If a field is not present in the source text, set it to null. Never invent values. Never guess at numbers."
Claude follows explicit null-handling instructions far more consistently than implicit ones.
Batch extraction pattern
When extracting from many documents, don't put more than one document per request — the model can conflate fields across documents, and you lose per-document error isolation. Instead, loop over documents and fire one tool-use call each:
async function extractInvoice(text) {
const res = 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-20250514",
max_tokens: 1024,
tools: [invoiceTool],
tool_choice: { type: "tool", name: "extract_invoice" },
messages: [{ role: "user", content: text }],
}),
});
const data = await res.json();
const toolUse = data.content.find(b => b.type === "tool_use");
return toolUse?.input ?? null;
}
Run these concurrently with a concurrency cap (5–10 is usually safe), and validate every response against your schema with a library like Zod or Ajv before writing to your database — the model is reliable, not infallible, and you want a hard validation layer regardless.
Validate, don't trust blindly
Even with forced tool use, always run a schema validator on the output before persisting it. Numbers can come back as strings in edge cases, enums can drift if your schema allows free-text fallback, and dates may not be normalized to ISO 8601 unless you explicitly request that format in the schema description. Add a format: "YYYY-MM-DD" note in the field description — Claude respects explicit format hints well.
Streaming extraction for long documents
For very long source documents (long contracts, transcripts), extraction latency matters. If you don't need the full tool call before starting downstream processing, streaming lets you render partial progress to a UI while the model finishes. See /docs/streaming for how streamed tool-use deltas are structured — note that with tool use, you typically still need the complete JSON block before parsing, so streaming here mostly helps perceived latency, not partial parsing.
If you're building extraction pipelines across a team — contract review, support ticket parsing, resume parsing — SubToAPI gives every team member their own sub_live_ key with shared usage visibility and per-seat billing, so you can see which pipeline or developer is driving token spend without sharing one shared secret. Check /pricing for Solo, Team, and Scale tiers, or start with a free trial at /signup.
FAQ
Does Claude API have a dedicated "JSON mode" like some other LLM APIs? Not as a separate flag. Instead, you get reliable structured output through forced tool use with an input_schema, which is more expressive than a generic JSON mode since it supports nested objects, enums, and arrays natively.
How do I stop Claude from adding extra fields not in my schema? Tool use schemas are strict by definition — the model populates exactly the properties you define. If you see extras, double-check you're using tool_choice to force that specific tool rather than letting the model choose freely.
What's the best way to extract data from scanned PDFs or images? Convert to text first (OCR) or use Claude's vision input to read the image directly, then apply the same tool-use extraction schema to the resulting text or image content block.