Free-form prose is ideal for a user-facing explanation but fragile for software decisions. Structured output lets an application request a response that conforms to a JSON Schema, while strict tool use constrains the parameters Claude sends to a tool. These solve different parts of the contract and can be combined.
Schemas reduce parsing ambiguity, but they do not make the underlying facts true. Validate semantic rules such as permissions, account ownership, date ranges, and business invariants after schema validation.
A schema can guarantee that a response has fields such as `category`, `priority`, and `needs_review`. It cannot guarantee that the category is correct for your business. Use schemas to remove parsing work, not to remove evaluation.
Prefer a small discriminated result over a giant optional object. The smaller contract is easier to version, test, and display.
Design fields around decisions. Include an allowed enum, a human-readable rationale for auditing, and a `needs_review` boolean or status. Avoid accepting a free-form field where an enum or typed identifier is required.
Do not place secrets or protected health information in a JSON schema description. Structured-output schema processing and caching have their own operational considerations in official documentation.
Run ordinary validators after decoding JSON. Check that a customer ID belongs to the session, a refund amount does not exceed the order total, and a date is valid for the product state. The model should not control defaults for privileged values.
If validation fails, preserve the raw redacted response for debugging, return a safe retry or review state, and measure the failure rate.
Tool schemas constrain how Claude asks your code to act. Your tool handler must still validate the request and authorize the action. A schema that accepts `recipient_id` does not mean the current user may message that recipient.
Use strict tool use when you need parameters to follow the schema, and keep destructive tools narrow, confirmed, and auditable.
Once you have decoded a structured response, treat it like input from any outside service. Convert it into your own type, validate the allowed values, and decide whether the next step is automatic or requires review.
This is where a small schema helps most: controller code can branch on a status instead of guessing from prose.
This is a real Messages API request. The JSON schema gives the route handler a predictable object instead of asking it to parse a sentence.
import json
import os
from anthropic import Anthropic
triage_schema = {
"type": "object",
"properties": {
"category": {"type": "string"},
"priority": {"type": "string"},
"needs_review": {"type": "boolean"},
},
"required": ["category", "priority", "needs_review"],
"additionalProperties": False,
}
client = Anthropic()
response = client.messages.create(
model=os.environ["CLAUDE_MODEL"],
max_tokens=200,
system="Classify the support ticket. Use needs_review when unsure.",
messages=[{"role": "user", "content": "I was charged twice this month."}],
output_config={"format": {"type": "json_schema", "schema": triage_schema}},
)
text = next(block.text for block in response.content if block.type == "text")
triage = json.loads(text)
print(triage)
Schema compliance is only the first check. This second layer rejects categories and priorities that the product does not support.
from dataclasses import dataclass
@dataclass(frozen=True)
class Triage:
category: str
priority: str
needs_review: bool
def validate_triage(value: Triage) -> bool:
return value.category in {"billing", "technical", "account"} and value.priority in {"low", "normal", "high"}
print(validate_triage(Triage("billing", "high", False)))
True
A narrow schema is easier to authorize than a generic “run action” tool. The tool handler must still compare order_id with the authenticated user.
{
"name": "get_order_status",
"description": "Read the status for one order owned by the authenticated customer.",
"input_schema": {
"type": "object",
"properties": {
"order_id": {"type": "string"}
},
"required": ["order_id"],
"additionalProperties": false
}
}
Even when a schema returns a string, normalize and compare it against the categories supported by your product.
def normalized_category(value: str) -> str:
category = value.strip().casefold()
return category if category in {"billing", "technical", "account"} else "other"
print(normalized_category(" Billing "))
billing
A practical refactor
0 of 2 completed
Yes. Structured output controls the final response shape while strict tools constrain tool input shape.
It prevents uncertainty from being silently converted into a confident automated decision.
Explore 500+ free tutorials across 20+ languages and frameworks.