Lesson 3 of 5 in Prompt Engineering & Structured Output

3.3 · Structured output via tool use

The reliable way to get JSON from Claude isn't 'ask for JSON in prose' — it's to define a tool whose input_schema matches your desired JSON and force its use. This lesson covers the tool-use pattern for structured output, retry-with-feedback for business rules, and nullable fields for missing data.

The reliable way to get JSON

Don't ask for JSON in prose. Define a tool whose input_schema matches the JSON you want, and force the model to call that tool. The model's input to the tool IS your structured output, and the schema is enforced server-side.

extract_tool = {
    "name": "extract_invoice",
    "description": "Return the extracted invoice data.",
    "input_schema": {
        "type": "object",
        "required": ["total", "currency", "line_items"],
        "properties": {
            "total": {"type": "number"},
            "currency": {"type": "string",
                "enum": ["USD", "CAD", "EUR", "GBP"]},
            "line_items": {
                "type": "array",
                "items": {
                    "type": "object",
                    "properties": {
                        "desc": {"type": "string"},
                        "amount": {"type": "number"}
                    }
                }
            }
        }
    }
}

r = client.messages.create(
    model="claude-sonnet-4-6", max_tokens=1024,
    tools=[extract_tool],
    tool_choice={"type": "tool",
                 "name": "extract_invoice"},  # force this tool
    messages=[{"role":"user","content":invoice_text}]
)

# r.content[0].input is your validated JSON
data = r.content[0].input

Why this beats prompt-only JSON

Prompt-only JSON drifts: missing fields, markdown fences around the block, extra prose before or after, broken quotes. All are common. Tool schemas are constraints the API enforces; the model can only produce input that fits the schema.

Retry-with-feedback loops

For business rules beyond what the schema can express (a total must equal the sum of line items, dates must be in range), validate after extraction and retry with feedback if invalid. This is much more reliable than trying to prompt the rule up front.

extract → validate → if invalid, re-prompt with the specific validation error → up to 3 retries

Nullable fields prevent fabrication

When source documents may not contain all required fields, mark those fields as nullable in the schema. Otherwise Claude, faced with a required field it can't fill, will fabricate a plausible value rather than refuse. Explicit nullability tells the model 'it's OK to leave this empty when the source doesn't have it.'

Good to know — Schemas prevent SYNTAX errors, not SEMANTIC ones. They won't stop a fabricated value; they just constrain its shape. Nullability is your knob to influence what the model does when data is missing.
# ❌ 'currency' is required — model may guess USD when unspecified
input_schema = {
    "required": ["total", "currency"],
    "properties": {
        "total": {"type": "number"},
        "currency": {"type": "string"}
    }
}

# ✅ nullable — model returns null when source doesn't specify
input_schema = {
    "required": ["total"],
    "properties": {
        "total": {"type": "number"},
        "currency": {"type": ["string", "null"]}
    }
}

When prompt-only JSON is fine

Short, flat objects with low stakes and human-in-the-loop review. For anything you'll parse into a typed system or a downstream pipeline, use tool-use.

Good to know — 'It works most of the time' isn't good enough for a pipeline processing 100K documents. The 1% is your failure mode.

Takeaways

  • Define a tool to get structured data
  • Tool schemas are enforced; prompt-only JSON drifts
  • Use tool_choice to force a specific tool
  • Retry with feedback for business rules beyond schema

Exam traps

Using a skill when a slash command fits
Skills auto-trigger on description matching. Slash commands are user-triggered by name. If the user always wants to explicitly invoke it, a slash command is clearer.
Writing a marketing-style skill description
The 'description' field is Claude's routing signal, not marketing copy. Write it like 'Use when...' and 'Do NOT use when...'
Giving a subagent 15+ tools
Scope each subagent to 4–5 role-relevant tools. Broad tool access degrades selection reliability.

Practice scenario

A team wants a repeatable 'prepare release' workflow that engineers explicitly invoke by typing /release. Which primitive is best?

← PreviousNext →