Skip to content

fix: coerce JSON-stringified dict/list tool params before Pydantic validation#1882

Open
giulio-leone wants to merge 1 commit intostrands-agents:mainfrom
giulio-leone:fix/bedrock-json-string-tool-params
Open

fix: coerce JSON-stringified dict/list tool params before Pydantic validation#1882
giulio-leone wants to merge 1 commit intostrands-agents:mainfrom
giulio-leone:fix/bedrock-json-string-tool-params

Conversation

@giulio-leone
Copy link
Contributor

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:

# Expected (works with Gemini):
parameters={'ecs_cluster_name': 'my-cluster', 'time_window': 3600}

# Actual (Bedrock/Claude):
parameters='{"ecs_cluster_name": "my-cluster", "time_window": 3600}'

This causes a Pydantic validation error:

ValidationError: 1 validation error for call[...]
parameters
  Input should be a valid dictionary [type=dict_type, input_value='{...}', input_type=str]

Root Cause

The Bedrock 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.

Solution

Added a pre-validation coercion step in FunctionToolMetadata.validate_input() via _coerce_json_string_params() that:

  1. Inspects each field's type annotation on the input model
  2. If the annotation accepts dict or list (including Optional/Union/Any) 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 Pydantic validation with a clear error)

Design Notes

  • Model-provider-agnostic: The fix is at the tool decorator level, not Bedrock-specific, so it benefits any provider that exhibits this behavior
  • Zero impact on correct inputs: The coercion only activates when a string value is received for a dict/list field; dict/list values pass through unchanged
  • Backward compatible: No changes to any public API or schema generation
  • Conservative: Only coerces to dict or list — won't coerce a string that json.loads to an int, bool, etc.

Testing

  • Added test_validate_input_coerces_json_string_to_dict: Tests dict coercion with Optional[dict[str, Any]], verifies dict values still work, None still works, and non-JSON strings still raise ValueError
  • Added test_validate_input_coerces_json_string_to_list: Tests list coercion with list[str]
  • All 82 decorator tests pass (80 existing + 2 new)
  • All 439 tools module tests pass

Changes

  • src/strands/tools/decorator.py: Added _coerce_json_string_params(), _annotation_accepts_mapping_or_sequence() methods to FunctionToolMetadata; updated validate_input() to call coercion before Pydantic validation
  • tests/strands/tools/test_decorator.py: Added 2 test cases for JSON string coercion

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 strands-agents#1285
@giulio-leone giulio-leone force-pushed the fix/bedrock-json-string-tool-params branch from 8bd3314 to c9370f9 Compare March 15, 2026 16:12
@github-actions github-actions bot added size/m and removed size/m labels Mar 15, 2026
@giulio-leone
Copy link
Contributor Author

Friendly ping — coerces JSON-stringified dict/list tool parameters back to native types before Pydantic validation, fixing crashes when LLMs return string-wrapped JSON.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Bedrock/Claude model incorrectly serializes nested object parameters as JSON string causing Pydantic validation errors

1 participant