Skip to main content

Tool Argument Validation: Preventing Agent Errors

Even with a precise JSON schema, models sometimes generate arguments that fail validation. A field might be the wrong type, a number might be out of bounds, or a required field might be missing. Argument validation is a defensive layer between tool invocation and execution. It catches these errors before they crash your tool, allowing your agent to recover gracefully instead of propagating a cryptic error message to the model. Proper validation reduces runtime exceptions by 95% and improves user experience.

What Is Tool Argument Validation?

Tool argument validation is the process of checking that the arguments a model passed to a tool conform to the schema and expected behavior before execution. It includes type checking, range validation, string length and format verification, required field checks, and custom business logic validation. A validation layer sits between the agent framework (which received the tool call) and the tool implementation (which executes the call).

Validation catches two categories of errors: (1) schema violations (type mismatch, missing required field, enum violation) and (2) semantic violations (logically invalid but schema-compliant, such as start_date after end_date, or a negative quantity). Schema violations should be caught at the framework level; semantic violations are caught by domain-specific validators.

Example: A tool expects a date range and both dates are schema-valid strings, but start_date is after end_date. The schema allows both, but the business logic forbids it. Your validator catches this and returns a clear error instead of returning meaningless results.

Validation Strategy

A three-layer approach is best:

Layer 1: Framework-level schema validation. After the model returns a tool call, immediately validate the arguments against the JSON Schema. This is usually automatic in modern agent frameworks (Anthropic SDK, LangChain, etc.) and catches type mismatches and enum violations.

Layer 2: Type coercion and normalization. Convert strings to numbers/booleans where safe (e.g., "true"True, "42"42). Trim whitespace, lowercase enum values, and parse ISO dates into date objects. This is lenient but safe.

Layer 3: Semantic validation. Apply domain-specific rules: is the number in a realistic range? Is the date not too far in the past or future? Does the email address pass a format check? Are start and end values in the correct order?

Here is a complete validation example:

from datetime import datetime
from typing import Dict, Any
import json

class ValidationError(Exception):
"""Raised when argument validation fails."""
pass

def validate_date_range_tool(arguments: Dict[str, Any]) -> Dict[str, Any]:
"""
Validate arguments for a date range query tool.
Raises ValidationError on any violation.
"""
# Layer 1: Schema-level checks (type, required fields)
if "start_date" not in arguments:
raise ValidationError("start_date is required")
if "end_date" not in arguments:
raise ValidationError("end_date is required")

start_str = arguments.get("start_date")
end_str = arguments.get("end_date")

if not isinstance(start_str, str) or not isinstance(end_str, str):
raise ValidationError("start_date and end_date must be strings")

# Layer 2: Normalization
try:
start_date = datetime.fromisoformat(start_str)
end_date = datetime.fromisoformat(end_str)
except ValueError as e:
raise ValidationError(f"Invalid date format: {e}")

# Layer 3: Semantic validation
if start_date > end_date:
raise ValidationError("start_date must be before end_date")

# Check if date range is reasonable (e.g., not more than 5 years)
delta = (end_date - start_date).days
if delta > 365 * 5:
raise ValidationError(f"Date range too large: {delta} days (max 5 years)")

if delta < 1:
raise ValidationError("Date range must be at least 1 day")

# Optional: limit to data available (e.g., not before 2000)
min_year = 2000
if start_date.year < min_year:
raise ValidationError(f"Dates before {min_year} not available")

# Return normalized arguments
return {
"start_date": start_date,
"end_date": end_date
}

# Test the validator
try:
result = validate_date_range_tool({
"start_date": "2024-01-01",
"end_date": "2024-12-31"
})
print("✓ Valid:", result)
except ValidationError as e:
print("✗ Error:", e)

# Test with invalid input
try:
result = validate_date_range_tool({
"start_date": "2024-12-31",
"end_date": "2024-01-01"
})
except ValidationError as e:
print("✗ Expected error:", e)

Validation Patterns for Common Argument Types

Strings:

def validate_string_argument(value, min_length=1, max_length=1000, pattern=None):
if not isinstance(value, str):
raise ValidationError(f"Expected string, got {type(value).__name__}")
if len(value) < min_length:
raise ValidationError(f"String too short: {len(value)} < {min_length}")
if len(value) > max_length:
raise ValidationError(f"String too long: {len(value)} > {max_length}")
if pattern and not re.match(pattern, value):
raise ValidationError(f"String does not match pattern: {pattern}")
return value.strip()

Numbers:

def validate_number_argument(value, minimum=None, maximum=None):
try:
num = float(value)
except (TypeError, ValueError):
raise ValidationError(f"Expected number, got {value}")
if minimum is not None and num < minimum:
raise ValidationError(f"Value {num} is below minimum {minimum}")
if maximum is not None and num > maximum:
raise ValidationError(f"Value {num} exceeds maximum {maximum}")
return num

Enums:

def validate_enum_argument(value, allowed_values):
if value not in allowed_values:
raise ValidationError(f"Invalid value {value}. Allowed: {allowed_values}")
return value

Arrays:

def validate_array_argument(value, item_type=str, max_items=None):
if not isinstance(value, list):
raise ValidationError(f"Expected array, got {type(value).__name__}")
if max_items and len(value) > max_items:
raise ValidationError(f"Array too large: {len(value)} > {max_items}")
for item in value:
if not isinstance(item, item_type):
raise ValidationError(f"Array item is {type(item).__name__}, expected {item_type.__name__}")
return value

Integration with Agent Loop

In your agent loop, wrap tool execution with validation:

def execute_tool_with_validation(tool_name, arguments, tool_definitions, tool_implementations):
"""Execute a tool after validating arguments."""

# Find tool definition
tool_def = next((t for t in tool_definitions if t["name"] == tool_name), None)
if not tool_def:
return {"error": f"Unknown tool: {tool_name}"}

# Get validator and implementation
validator = tool_implementations[tool_name].get("validator")
impl = tool_implementations[tool_name]["impl"]

# Validate
try:
if validator:
validated_args = validator(arguments)
else:
validated_args = arguments
except ValidationError as e:
return {"error": f"Invalid arguments: {str(e)}"}

# Execute
try:
result = impl(**validated_args)
return result
except Exception as e:
return {"error": f"Execution failed: {str(e)}"}

Then in the agent loop:

# ... after model outputs tool_use ...

tool_result = execute_tool_with_validation(
tool_call.name,
tool_call.input,
tools,
tool_implementations
)

messages.append({
"role": "user",
"content": [{
"type": "tool_result",
"tool_use_id": tool_call.id,
"content": json.dumps(tool_result)
}]
})

When validation fails, the error message is appended to the conversation, and the model sees it and may retry with corrected arguments, ask the user for clarification, or report the error.

Comparison: Validation Strategies

StrategyOverheadError Catch RateUser Impact
Schema only (framework)Minimal40–60%Runtime crashes, poor UX
Schema + type coercionLow70–85%Better recovery, clearer errors
Full three-layerModerate95%+Graceful degradation, clear messages

Key Takeaways

  • Argument validation is a three-layer approach: schema validation, type coercion, and semantic checks.
  • Schema violations are caught at the framework level; semantic violations require custom validators.
  • Validators should catch errors and return clear messages, not raise unhandled exceptions.
  • Integrate validation into your agent loop before tool execution.
  • Proper validation reduces runtime errors by 95% and improves agent reliability.

Frequently Asked Questions

Should I validate in the tool or in the agent framework?

Ideally, both. Validate in the agent framework before execution to catch errors early and return a clear error message to the model. Also validate in the tool itself as a defensive measure (never trust inputs). Framework-level validation is for the agent; tool-level validation is for safety.

What if validation passes but the tool still fails?

Execution errors (database timeout, API rate limit, permission denied) are different from validation errors. Catch them separately: if validation passes but execution fails, return an error message describing the runtime issue. The model will see it and may retry with backoff or try a different tool.

Can I let the model retry after a validation error?

Yes. When validation fails, append a tool_result with the error message. The model reads the error and may call the tool again with adjusted arguments. This loop can repeat until the arguments are valid or the model gives up.

How do I test validators?

Write unit tests for each validator using pytest or unittest. Test valid inputs, boundary values (min/max), invalid types, and edge cases (empty strings, zero, negative numbers). Aim for 100% code coverage of the validator.

Is there a performance cost?

Validators add minimal overhead—typically <5ms per call. If your tool is I/O bound (database query, API call), validation cost is negligible. For high-throughput scenarios (thousands of calls/sec), cache validators or use compiled validators.

Further Reading