Validate a Tool Call Against Its Schema
Problem Statement
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.
Background
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".
Your Task
Implement:
def validate_tool_call(schemas, call):
- schemas: dict mapping tool name -> schema dict as above.
- call: dict {"name": str, "arguments": dict}.
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.
Input/Output Format
Returns a dict with keys ok (bool) and errors (sorted list of strings).
Sample
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']}
Example:
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.
Constraints:
1 <= len(schemas) <= 50; each schema has at most 20 properties- An unknown tool returns exactly
["unknown_tool:<name>"]and stops - At most one error per argument:
type:suppresses theenum:check True/Falseare valid only for"type": "boolean"- never forintegerornumberintis an acceptablenumber;floatis not an acceptableinteger- The
errorslist must be sorted ascending
1. Background Knowledge
This problem centers on schema validation, a critical component in building robust AI agents and API interfaces. When an LLM generates a tool call, it produces a JSON-like structure that claims to adhere to a specific contract (the schema). Your code acts as the gatekeeper, verifying that this claim is true before executing potentially dangerous or expensive operations. This separation of concerns ensures that business logic remains clean and that errors are reported in a structured, machine-readable format that the agent can parse and correct.
The core concept here is JSON Schema validation, specifically focusing on object properties. A schema defines the shape of data: which fields are mandatory (required), what types they must be (type), and optional constraints like allowed values (enum). In Python, type checking requires nuance because of inheritance hierarchies. For instance, bool is a subclass of int. Therefore, isinstance(True, int) returns True. However, in strict schema validation, a boolean value is not a valid integer. You must explicitly check for bool first and reject it if the schema expects an integer or number.
Another key concept is error aggregation. Unlike traditional programming where an exception might halt execution immediately, schema validators often collect all errors to provide comprehensive feedback. This allows the agent to fix multiple issues in a single retry. The order of checks matters: existence of the tool, presence of required fields, absence of unexpected fields, and finally, type/value correctness.
2. Algorithm Approach
The general approach is a multi-stage validation pipeline. You should process the input call through a series of distinct checks, accumulating errors in a list.
- Tool Existence Check: Verify the tool name exists in the schemas dictionary. If not, return immediately with a single error.
- Structural Validation: Iterate through the schema's properties and the call's arguments to identify missing required fields and unexpected fields.
- Type & Value Validation: For each present argument, verify its type against the schema definition. If the type is correct and an enum is defined, verify the value is in the allowed list.
- Result Formatting: Sort the accumulated errors alphabetically and return the final dictionary.
This approach separates concerns: structural integrity (keys) is checked before semantic integrity (values).
3. Step-by-Step Strategy
- Initialize: Create an empty list errors to store error strings.
- Check Tool Name:
- Extract tool_name from call["name"].
- If tool_name is not in schemas, return {"ok": False, "errors": [f"unknown_tool:{tool_name}"]} immediately.
- Extract Schema Details:
- Get the specific schema for the tool.
- Identify properties (declared fields), required (mandatory fields), and arguments (provided fields).
- Check Missing Required Fields:
- Iterate through the required list.
- If a required field is not in arguments, add f"missing:{field}" to errors.
- Check Unexpected Fields:
- Iterate through the keys in arguments.
- If a key is not in properties, add f"unexpected:{key}" to errors.
- Check Types and Enums:
- Iterate through the keys in arguments that are in properties.
- Get the expected type from the schema.
- Type Checking Logic:
- Map JSON types to Python types (string->str, integer->int, etc.).
- Crucial: If the expected type is integer or number, explicitly check if the value is a bool. If it is, it's a type error.
- Use isinstance() for other types.
- If the type is wrong, add f"type:{key}" to errors and skip the enum check for this field.
- Enum Checking Logic:
- If the type is correct and the schema defines an enum, check if the value is in that list.
- If not, add f"enum:{key}" to errors.
- Finalize:
- Sort the errors list.
- Return {"ok": len(errors) == 0, "errors": errors}.
4. Common Pitfalls
- Boolean/Integer Confusion: The most common mistake is using isinstance(val, int) for integer validation without checking for bool first. Remember: True is an int in Python. You must reject bool values for integer/number types.
- Order of Operations: Ensure you check for missing and unexpected fields before or independently of type checks. The problem states that type errors suppress enum checks, but missing/unexpected errors are structural and should always be reported if applicable.
- Short-Circuiting: Do not forget to return immediately if the tool is unknown. Do not attempt to validate arguments for a non-existent tool.
- Sorting: The final list of errors must be sorted alphabetically. Forgetting this will cause test failures even if the logic is correct.
- Type Mapping: Ensure you handle number correctly. In Python, both int and float are valid numbers. A naive isinstance(val, float) will fail for integers. Use isinstance(val, (int, float)) but again, exclude bool.
5. Time & Space Complexity
- Time Complexity: O(NlogN), where N is the number of arguments in the call. The validation steps (checking keys, types, enums) are linear O(N) relative to the number of arguments. The dominant factor is the final sorting of the error list, which takes O(NlogN).
- Space Complexity: O(N), to store the list of errors. In the worst case, every argument generates an error. The schema lookup is O(1) assuming hash map implementation for dictionaries.