The model does not call your tool - it emits a JSON object claiming to call your tool. Everything between that claim and your code is your responsibility. A validator sitting on the boundary turns a malformed call into a precise, machine-readable error the agent can actually recover from, instead of a stack trace inside your business logic.
A tool schema is a JSON-Schema-shaped object:
{
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string"},
"days": {"type": "integer"},
"units": {"type": "string", "enum": ["c", "f"]}
},
"required": ["city"]
}
}
Validation checks four things, in this order per argument: the tool exists, every required parameter is present, no undeclared parameters were passed, and every declared parameter has the right type (and, if an enum is declared, an allowed value).
The subtle one: in Python bool is a subclass of int, so a naive isinstance(True, int) accepts True for an integer parameter. It is not one. A boolean value is valid only for "type": "boolean".
Implement:
def validate_tool_call(schemas, call):
Return {"ok": bool, "errors": list[str]} where errors use these exact formats:
| Situation | Error string | |---|---| | tool name not in schemas | unknown_tool:<name> | | required parameter absent | missing:<param> | | argument not declared in properties | unexpected:<param> | | declared parameter, wrong type | type:<param> | | right type, value not in enum | enum:<param> |
Rules: an unknown tool short-circuits - return that single error and nothing else. Otherwise collect every error, and report at most one error per argument (a type error suppresses the enum check for that argument). Sort the final error list ascending before returning. ok is True only when the list is empty.
Type names map to Python as: string->str, integer->int, number->int or float, boolean->bool, array->list, object->dict. A parameter spec with no "type" accepts anything.
Returns a dict with keys ok (bool) and errors (sorted list of strings).
schemas = {"get_weather": {"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}, "days": {"type": "integer"}},
"required": ["city"]}}}
print(validate_tool_call(schemas, {"name": "get_weather",
"arguments": {"days": True, "tz": "UTC"}}))
# {'ok': False, 'errors': ['missing:city', 'type:days', 'unexpected:tz']}
schemas = {'get_weather': {'parameters': {'type':'object','properties': {'city': {'type':'string'}, 'days': {'type':'integer'}}, 'required': ['city']}}}
print(validate_tool_call(schemas, {'name': 'get_weather', 'arguments': {'days': True, 'tz': 'UTC'}})){'ok': False, 'errors': ['missing:city', 'type:days', 'unexpected:tz']}city is required but absent -> missing:city. days is declared integer but got True, and a bool is not an integer -> type:days. tz is not declared at all -> unexpected:tz. Sorting the three strings ascending gives missing, type, unexpected.
1 <= len(schemas) <= 50; each schema has at most 20 properties["unknown_tool:<name>"] and stopstype: suppresses the enum: checkTrue/False are valid only for "type": "boolean" - never for integer or numberint is an acceptable number; float is not an acceptable integererrors list must be sorted ascending