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].inputWhy 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.'
# ❌ '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.
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
Practice scenario
A team wants a repeatable 'prepare release' workflow that engineers explicitly invoke by typing /release. Which primitive is best?