JSON schemas
Trace a model-requested tool call through schema validation, execution and a returned observation.
THE IDEA
JSON schemas
A JSON schema describes the allowed arguments: which fields exist, their types, allowed values (enum), numeric limits, which fields are required, and whether extra fields are allowed. Each rule is explicit. A property that is declared but not listed in required may be omitted, and unless you say otherwise an unknown field may or may not be tolerated. Decide both on purpose.
Declaring a property says what it must look like if present. Whether it must be present is a separate list. Untick "qty in required" and send {note: 5} to see the model get away with omitting it.
Predict first. Does declaring an integer field automatically require that field to be present?
Chapter context
A tool call is a structured request for the application to perform an operation. The model proposes arguments; the surrounding application decides whether the operation is valid and permitted and is responsible for executing it.
TRY IT YOURSELF
Live lab
In the schema below, unit is allowed but optional. Run it once and watch what the model sends. Then tighten the contract so the host rejects any call without a valid unit.
TRY IT YOURSELF
Make a field required
In the schema below, unit is allowed but optional. Run it once and watch what the model sends. Then tighten the contract so the host rejects any call without a valid unit.
Edit the schema so unit is required and limited to its enum. Then get a run where every call validates and includes unit.
Task given to the model: What's the weather in Tokyo?
This loop as code
import anthropic
client = anthropic.Anthropic() # reads ANTHROPIC_API_KEY
TOOLS = [ # name, description and input_schema are the "Tool definition"
{"name": t["name"], "description": t["description"], "input_schema": t["schema"]}
for t in [
{
"name": "get_weather",
"description": "Get the current weather for a city.",
"schema": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "City name"
},
"unit": {
"type": "string",
"enum": [
"celsius",
"fahrenheit"
]
}
},
"required": [
"city"
]
}
}
]
]
messages = [{"role": "user", "content": "What's the weather in Tokyo?"}]
for step in range(5): # Step budget
response = client.messages.create(model="claude-haiku-4-5", max_tokens=600, tools=TOOLS, messages=messages)
messages.append({"role": "assistant", "content": response.content})
calls = [b for b in response.content if b.type == "tool_use"]
if not calls:
break # final answer
results = []
for call in calls: # the model only PROPOSES these
problems = validate(call.input, schema_for(call.name)) # your host-side check
if problems:
output, is_error = "Invalid arguments: " + "; ".join(problems), True
else:
output, is_error = run_tool(call.name, call.input) # your code runs it
results.append({"type": "tool_result", "tool_use_id": call.id, "content": output, "is_error": is_error})
messages.append({"role": "user", "content": results})Update the controls above and this code changes with them. Run it yourself with the key in an environment variable.Exercises
Part 1. Do it hands-on
Count schema problems by hand, then write a validator with ranges and a function that tightens a schema.
Declaring a property says what it must look like if present. Whether it must be present is a separate list. Untick "qty in required" and send {note: 5} to see the model get away with omitting it.
- To do: Schema: qty (integer 1–100, required), note (string); extra fields not allowed. Call {"note": 5}: number of problems?
- To do: Call {"qty": 0}: number of problems?
- To do: Call {"qty": 3, "extra": 1} with additionalProperties false: number of problems?
- To do: The same call when additionalProperties is true?
- To do: Does declaring qty as an integer property automatically make it required? Type yes or no
Part 2. Do it in code
- validate_args(schema, args): as before (missing, unexpected, type, enum), plus "min:<name>" when a numeric value is below the property's minimum and "max:<name>" when it is above the maximum (checked only after the type is fine, min before max).
- tighten_required(schema, field): a NEW schema (deep enough that the original is not changed) with field added to required. ValueError if field is not a declared property; adding a field that is already required changes nothing.
- forbid_extras(schema): a NEW schema with additionalProperties False.
- diff_problems(schema_a, schema_b, args): the problems that appear with schema_b but not with schema_a (a list, in schema_b's order).
Both the live lab and the exercises must be solved to finish this lesson.
Stuck, or something not working? You can come back to it any time.
CHECK YOUR UNDERSTANDING
Does declaring an integer field automatically require that field to be present?
Chapter reference: math, worked example & code
This section belongs to the whole chapter, so it is the same on every lesson in it and may cover a different lesson's topic. The code for the experiment you just ran is inside the experiment itself.
Math and notation
A useful execution model is: requested operation + validated arguments + permission decision → execution result. Schema validation establishes a data-shape contract; authorization establishes whether this caller may perform this operation. Neither implies the other.
Worked example
A calculator tool requires numeric a and b and returns a+b. For a=2,b=3, its result is 5. If the request instead asks for an unknown field or omits b, reject or repair the request according to the contract before executing. A retry of this pure calculation is harmless; that does not establish the same property for a side-effecting tool.
Reference code
The following example uses the Python standard library and does not call a remote service.
def add(a,b):
if type(a) not in (int,float) or type(b) not in (int,float):
raise ValueError('numeric inputs required')
return a+b
assert add(2,3) == 5
try:
add('2',3)
except ValueError:
pass
else:
raise AssertionError('invalid input accepted')
Python examples are provided for study and local execution; this page does not execute Python.
Common mistakes
A model proposal is not permission. JSON schema shape validation is not business-rule validation. Tool outputs must not silently become trusted instructions.