← Blog

How to Build a JSON Extraction Tool with Claude

2026-10-29 · 5 min read · SubToAPI Team

Building a JSON extraction tool with Claude means getting the model to reliably turn unstructured text — emails, PDFs, support tickets, invoices — into structured data your application can use. The short answer: you combine a tightly scoped prompt, a defined JSON schema, Claude's tool use (function calling) feature for guaranteed structure, and a validation layer that catches the edge cases the model gets wrong.

This guide walks through the full process, from prompt design to production-ready validation, so you end up with an extraction pipeline that doesn't silently break when the input text gets weird.

Why JSON Extraction Is Harder Than It Looks

Asking an LLM to "return JSON" works fine in a demo and fails in production. Common failure modes:

A real extraction tool needs to handle all of these, not just the happy path.

Step 1: Define Your Schema First

Before writing any prompt, write the exact JSON structure you want. Be explicit about types, nullable fields, and enums.

{
  "invoice_number": "string",
  "vendor_name": "string",
  "total_amount": "number",
  "currency": "string (ISO 4217, e.g. USD)",
  "line_items": [
    { "description": "string", "quantity": "number", "unit_price": "number" }
  ],
  "due_date": "string (YYYY-MM-DD) or null"
}

This schema becomes both your prompt instruction and your validation target later.

Step 2: Use Tool Use Instead of Prompting for JSON

The most reliable way to extract structured JSON from Claude is tool use, not plain prompting. Instead of asking the model to "respond in JSON," you define a tool with a JSON schema as its input, and Claude populates that schema directly — no markdown fences, no preamble text to strip out.

{
  "model": "claude-sonnet-4",
  "max_tokens": 1024,
  "tools": [
    {
      "name": "extract_invoice",
      "description": "Extract structured invoice data from raw 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"]
            }
          },
          "due_date": { "type": ["string", "null"] }
        },
        "required": ["invoice_number", "vendor_name", "total_amount", "currency"]
      }
    }
  ],
  "tool_choice": { "type": "tool", "name": "extract_invoice" },
  "messages": [
    { "role": "user", "content": "Extract the invoice data from this text:\n\n<raw invoice text>" }
  ]
}

Forcing tool_choice to the specific tool guarantees Claude returns a structured tool call rather than a free-text response. If you're building this against SubToAPI, the request shape is the same — see /docs/tools for the full tool use reference and /docs/messages for the base request format.

Step 3: Parse and Validate the Response

Even with tool use, add a validation layer. Use a schema validator (Zod in JavaScript, Pydantic in Python) to catch type mismatches before they hit your database.

import { z } from "zod";

const InvoiceSchema = z.object({
  invoice_number: z.string(),
  vendor_name: z.string(),
  total_amount: z.number(),
  currency: z.string().length(3),
  line_items: z.array(
    z.object({
      description: z.string(),
      quantity: z.number(),
      unit_price: z.number()
    })
  ),
  due_date: z.string().nullable()
});

const toolCall = response.content.find(block => block.type === "tool_use");
const result = InvoiceSchema.safeParse(toolCall.input);

if (!result.success) {
  console.error("Validation failed:", result.error.issues);
  // retry, flag for manual review, or fall back to a stricter prompt
}

This catches the cases where Claude returns a string where a number was expected, or drops a required field on a particularly messy document.

Step 4: Handle Retries and Edge Cases

Build in a retry path for validation failures. A second attempt with a slightly adjusted prompt — explicitly restating the field that failed — resolves most issues:

if (!result.success) {
  const retryPrompt = `The previous extraction was missing or malformed fields: ${result.error.issues.map(i => i.path.join(".")).join(", ")}. Re-extract carefully, ensuring these fields are present and correctly typed.`;
  // send a follow-up request with retryPrompt appended
}

For high-volume pipelines, log every validation failure with the raw input text attached. Patterns in failures usually point to a schema that's ambiguous (e.g., dates in multiple formats in the source documents) rather than a model problem.

Step 5: Stream for Large Documents

If you're extracting from long documents — multi-page contracts, long transcripts — streaming the response lets you start processing partial tool-call JSON before generation finishes, which matters for latency-sensitive pipelines. See /docs/streaming for details on handling streamed tool-use events.

Running This on SubToAPI

If your team already has Claude access through a subscription and wants to expose it as a proper API for this kind of pipeline, SubToAPI turns that access into standard HTTPS endpoints with application API keys (sub_live_...), full tool use support, streaming, and usage metadata per key — useful when you have multiple extraction jobs (invoices, support tickets, resumes) running under different keys for cost tracking. Plans start at €9/month for solo use, with team seats at €19 and €49 for higher-volume scale usage. Check /pricing or start with a free trial at /signup, and /docs/quickstart to get your first extraction request running in a few minutes.

FAQ

Does Claude guarantee valid JSON output?

Not with plain prompting — the model can add text around the JSON or produce malformed structures. Using tool use with a defined input_schema and tool_choice set to that tool is the closest thing to a guarantee, but you should still validate the response with a schema library before trusting it downstream.

What's the difference between prompting for JSON and using tool use?

Prompting for JSON relies on the model following instructions in natural language, which can drift on edge cases. Tool use defines a strict schema the model fills in as structured function-call input, which is far more consistent and doesn't require stripping markdown fences or explanatory text from the output.

How do I handle documents longer than Claude's context window?

Chunk the document and extract per-chunk, then merge results, or summarize non-essential sections first to reduce token count. For documents with clearly delimited sections (like multi-page invoices), extracting per-page and merging line items is usually more reliable than one giant extraction call.

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 →