fix: coerce JSON-stringified dict/list tool params before Pydantic validation#1882
fix: coerce JSON-stringified dict/list tool params before Pydantic validation#1882giulio-leone wants to merge 1 commit intostrands-agents:mainfrom
Conversation
8bd3314 to
c9370f9
Compare
|
Friendly ping — coerces JSON-stringified dict/list tool parameters back to native types before Pydantic validation, fixing crashes when LLMs return string-wrapped JSON. |
Bedrock/Claude sometimes serializes nested object or array tool-call
parameters as JSON strings instead of native Python dicts/lists. This
causes Pydantic validation errors like:
Input should be a valid dictionary [type=dict_type, input_value='{...}', input_type=str]
Root cause: the Converse API returns tool_use.input as a parsed dict,
but for nested object/array parameters the model may stringify the
value before embedding it in the outer dict. The SDK's Pydantic-based
validate_input() then receives a str where it expects a dict/list and
rejects it.
The fix adds a pre-validation coercion step in
FunctionToolMetadata.validate_input() that:
1. Inspects each field's type annotation on the input_model
2. If the annotation accepts dict or list (including Optional/Union)
and the incoming value is a string, attempts json.loads()
3. Only replaces the value if deserialization produces a dict or list
4. Leaves non-JSON strings untouched (they still fail validation)
This is model-provider-agnostic and has zero impact on correctly-typed
inputs — the coercion only activates when a string value is received
for a dict/list field.
Closes #1285
|
Refreshed onto Root cause confirmed still live: Bedrock/Claude sometimes serializes nested Fix: Runtime proof on rebased branch # Input: JSON-string-encoded dict and list (as Bedrock sends them)
meta.validate_input({'params': '{"key": "val"}', 'items': '["a", "b"]'})
# Output:
Coerced dict: {'key': 'val'}
Coerced list: ['a', 'b']
Types: dict listTest results:
|
c9370f9 to
9009177
Compare
Issue
Closes #1285
Problem
When using strands with Bedrock/Claude and MCP tools that have nested object parameters (e.g.,
parameters: dict[str, Any] | None), the model incorrectly serializes the parameter value as a JSON string instead of a native Python dictionary:This causes a Pydantic validation error:
Root Cause
The Bedrock Converse API returns
tool_use.inputas a parsed dict, but for nested object/array parameters the model may stringify the value before embedding it in the outer dict. The SDK's Pydantic-basedvalidate_input()then receives astrwhere it expects adict/listand rejects it.Solution
Added a pre-validation coercion step in
FunctionToolMetadata.validate_input()via_coerce_json_string_params()that:dictorlist(includingOptional/Union/Any) and the incoming value is a string, attemptsjson.loads()dictorlistDesign Notes
dictorlist— won't coerce a string thatjson.loadsto an int, bool, etc.Testing
test_validate_input_coerces_json_string_to_dict: Tests dict coercion withOptional[dict[str, Any]], verifies dict values still work, None still works, and non-JSON strings still raiseValueErrortest_validate_input_coerces_json_string_to_list: Tests list coercion withlist[str]Changes
src/strands/tools/decorator.py: Added_coerce_json_string_params(),_annotation_accepts_mapping_or_sequence()methods toFunctionToolMetadata; updatedvalidate_input()to call coercion before Pydantic validationtests/strands/tools/test_decorator.py: Added 2 test cases for JSON string coercion