-
Notifications
You must be signed in to change notification settings - Fork 603
Description
Summary
Replace the custom validation flow in src/strands/tools/decorator.py with Pydantic's built-in @validate_call decorator to simplify the codebase, improve maintainability, and unlock full Pydantic validation features for tool authors.
Motivation
The current tool decorator implements ~130 lines of custom validation logic (lines 80-220) that:
- Duplicates Pydantic functionality - Manually creates Pydantic models, extracts type hints, and handles validation
- Has JSON serialization issues - Recently discovered that
model_dump()withoutmode='json'causes failures when storing data inagent.state - Blocks Pydantic features - Explicitly raises
NotImplementedErrorforpydantic.Fieldconstraints inAnnotatedtypes (lines 139-160) - Causes type hint inconsistency - Parameters typed as Pydantic models are actually dicts at runtime (see issue [FEATURE] Strict Typing option for Tool Decorator #917)
Recent Bug Example
class BankCheck(BaseModel):
date: date # datetime.date object
amount: Decimal
# Current decorator flow:
validated = input_model(**input_data)
return validated.model_dump() # Keeps date/Decimal as Python objects!
# When tool tries to store in state:
agent.state.set("result", extraction_dict) # JSON validation fails!
# Error: Value is not JSON serializable: dictWorkaround applied: Changed to model_dump(mode='json'), but this is a band-aid on a deeper architectural issue.
Type Hint Inconsistency (Issue #917)
class Person(BaseModel):
name: str
age: int
@tool
def create_person(input: Person) -> Person:
input.name # ❌ Fails - input is a dict, not a Person instance!
input["name"] # ✅ Works - but type hints are liesProposed Solution
Replace custom validation with Pydantic's @validate_call:
from pydantic import validate_call, ConfigDict
def create_validated_tool_function(func: Callable, special_params: set[str]) -> Callable:
"""
Wrap function with validate_call, excluding special parameters.
Args:
func: The function to validate
special_params: Parameters to exclude (agent, tool_context, self, cls)
"""
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
def validated_wrapper(**validated_kwargs):
return validated_kwargs
def final_wrapper(**all_kwargs):
validated_kwargs = {k: v for k, v in all_kwargs.items() if k not in special_params}
special_kwargs = {k: v for k, v in all_kwargs.items() if k in special_params}
validated_data = validated_wrapper(**validated_kwargs)
return func(**validated_data, **special_kwargs)
return final_wrapperBenefits
- ~100 lines of code removed - Delete
_create_input_model(),_extract_annotated_metadata(), custom validation logic - Full Pydantic feature support - Users can now use
Field(ge=0, le=100), custom validators, etc. - Better error messages - Leverage Pydantic's detailed validation errors
- Automatic JSON serialization - No more manual
mode='json'workarounds - Type hint correctness - Parameters typed as Pydantic models are actual model instances at runtime
- Future-proof - Automatically benefits from Pydantic improvements
- Aligns with Development Tenets:
- Embrace common standards (Tenet ci: update ruff requirement from <0.5.0,>=0.4.4 to >=0.4.4,<0.12.0 #6) - Use Pydantic's standard validation approach
- Simple at any scale (Tenet ci: update sphinx requirement from <6.0.0,>=5.0.0 to >=5.0.0,<9.0.0 #1) - Less custom code = simpler to understand
- Extensible by design (Tenet ci: update mypy requirement from <1.0.0,>=0.981 to >=0.981,<2.0.0 #2) - Unlock full Pydantic validation capabilities
Example: What This Enables
from pydantic import Field
from typing import Annotated
# Currently raises NotImplementedError ❌
# After this change, works perfectly ✅
@tool
def my_tool(
age: Annotated[int, Field(ge=0, le=120, description="Person's age")],
email: Annotated[str, Field(pattern=r"^[\w\.-]+@[\w\.-]+\.\w+$")],
) -> dict:
"""Tool with field validation constraints."""
return {"age": age, "email": email}
# Type hints now reflect reality ✅
@tool
def create_person(input: Person) -> Person:
input.name # ✅ NOW WORKS - input IS a Person instance!
return Person(name=input.name.upper(), age=input.age)Implementation Plan
Phase 1: Parameter Exclusion Wrapper
Create wrapper function that applies @validate_call while excluding special parameters (agent, tool_context, self, cls)
Phase 2: Simplify FunctionToolMetadata
Remove validation methods:
- Delete
_create_input_model()(~50 lines) - Delete
_extract_annotated_metadata()(~50 lines) - Delete
validate_input()method - Keep only schema extraction and metadata generation
Phase 3: Update DecoratedFunctionTool
- Remove calls to
metadata.validate_input() - Handle Pydantic's
ValidationErrorin.stream()method
Phase 4: Testing
- Ensure all existing tests pass (no breaking changes)
- Add tests for
Field()constraints (previously unsupported) - Test complex scenarios (nested models, datetime/Decimal, etc.)
Migration Impact
For users: No breaking changes expected. All existing tools continue to work, but now behave more intuitively.
New capability unlocked: Users can now use full Pydantic validation:
@tool
def process_data(
count: Annotated[int, Field(gt=0, le=1000)], # Now works!
name: Annotated[str, Field(min_length=1, max_length=100)], # Now works!
) -> dict:
...Related Issues
This proposal fully resolves issue #917 - [FEATURE] Strict Typing option for Tool Decorator.
Issue #917 requests that tool parameters typed as Pydantic models should actually BE Pydantic model instances at runtime, not dicts. The issue specifically suggests using @validate_call to achieve this.
Our proposal implements exactly this, making strict typing the default behavior (as #917 comments suggest should happen in 2.0).
Before (Current Behavior - Issue #917 Problem):
@tool
def create_person(input: Person) -> Person:
input.name # ❌ Fails - input is a dict
input["name"] # ✅ Works - but type hints lieAfter (With This Proposal - Issue #917 Solved):
@tool
def create_person(input: Person) -> Person:
input.name # ✅ Works - input IS a Person instance!By adopting @validate_call throughout, we solve #917's core issue while also unlocking additional benefits (Field validation, JSON serialization, simpler codebase).
Closes #917
References
- Pydantic validate_call docs
- Current code:
src/strands/tools/decorator.pylines 80-374 - Recent bug fix: Added
mode='json'workaround for JSON serialization - Related: Lines 139-160 explicitly block
Field()inAnnotated - Issue [FEATURE] Strict Typing option for Tool Decorator #917: Strict typing request
Open Questions
- Should we expose Pydantic's
validate_returnoption to tool authors? - Do we need a deprecation period for any edge cases?
- Should parameter exclusion wrapper be a public utility?
This proposal aligns with our development tenets, particularly "Embrace common standards" and "Simple at any scale". By leveraging Pydantic's standard validation approach instead of custom code, we make Strands simpler to maintain and more powerful for users.