Skip to content

fix: serialize nested dict tool output to prevent TypeError (#6267)#6332

Open
C1-BA-B1-F3 wants to merge 1 commit into
crewAIInc:mainfrom
C1-BA-B1-F3:fix/nested-dict-tool-output
Open

fix: serialize nested dict tool output to prevent TypeError (#6267)#6332
C1-BA-B1-F3 wants to merge 1 commit into
crewAIInc:mainfrom
C1-BA-B1-F3:fix/nested-dict-tool-output

Conversation

@C1-BA-B1-F3

@C1-BA-B1-F3 C1-BA-B1-F3 commented Jun 25, 2026

Copy link
Copy Markdown

Summary

When a custom tool returns a nested dictionary, the agent's execution loop crashes with a TypeError (e.g., unhashable type: 'dict') because str() produces a Python repr instead of valid JSON.

This fix adds json.dumps() serialization for dict and list outputs in both:

  • _format_tool_output_for_agent() in structured_tool.py
  • _format_result() in tool_usage.py

Also fixes #6072 by showing task output before prompting for human feedback when verbose=False.

Changes

  • lib/crewai/src/crewai/tools/structured_tool.py: JSON serialize dict/list in _format_tool_output_for_agent
  • lib/crewai/src/crewai/tools/tool_usage.py: JSON serialize dict/list in _format_result
  • lib/crewai/src/crewai/agents/crew_agent_executor.py: Show output before human feedback prompt
  • lib/crewai/tests/tools/test_structured_tool.py: Add tests for dict/list/string tool outputs

Test plan

  • Added unit tests for dict, list, and string tool outputs
  • Verify existing tests pass

Closes #6267
Closes #6072

Summary by CodeRabbit

  • Bug Fixes
    • Tool outputs that are dictionaries or lists are now shown to agents as valid JSON instead of Python-style string representations.
    • Nested structures are preserved and can be parsed reliably.
    • When output cannot be JSON-serialized, the app now falls back to a readable string format consistently.

@corridor-security corridor-security Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary: This PR changes tool output formatting to JSON-serialize dict/list results and displays task output before human feedback when verbose logging is disabled; no exploitable security vulnerabilities were identified.

Risk: Low risk. The changes do not introduce new authentication, authorization, external request, file path, SQL, or command execution surfaces, and the added behavior only affects internal output formatting/display.

@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Tool output formatting now serializes dict and list results to JSON in both structured and native tool paths, with string fallback for non-serializable values. The tests were updated to cover nested dicts, lists, and custom object fallbacks.

Changes

Tool result JSON formatting

Layer / File(s) Summary
Structured tool output
lib/crewai/src/crewai/tools/structured_tool.py, lib/crewai/tests/tools/test_structured_tool.py
CrewStructuredTool now routes non-Pydantic and fallback outputs through a JSON-string helper, and tests cover dict, nested dict, list, and custom object results.
Native tool output
lib/crewai/src/crewai/utilities/agent_utils.py, lib/crewai/tests/tools/test_base_tool.py
format_native_tool_output_for_agent() now JSON-serializes dict and list results when no callable formatter is available, and BaseTool/@tool tests expect JSON strings for non-Pydantic and validation-failure cases.

Suggested reviewers

  • lucasgomide
  • greysonlalonde
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning #6267 is addressed by JSON-serializing dict/list tool outputs, but #6072's non-verbose human_input feedback flow is not implemented. Add a verbose-independent result display or embed the task output in the human_input prompt so #6072 is satisfied.
Docstring Coverage ⚠️ Warning Docstring coverage is 41.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main fix: serializing nested dict tool output to avoid TypeError.
Out of Scope Changes check ✅ Passed The changes stay focused on tool-output serialization and related tests; no unrelated code paths or broad refactors appear.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
lib/crewai/tests/tools/test_structured_tool.py (1)

527-582: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a fallback serialization test case.

Please add one test where json.dumps(..., default=str) still fails (e.g., unsupported dict key type) and assert fallback to str(...). That will cover the new exception branch.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/crewai/tests/tools/test_structured_tool.py` around lines 527 - 582, The
new structured tool output tests cover JSON serialization for dicts, lists, and
strings, but they do not exercise the fallback path when json.dumps(...,
default=str) still fails. Add a test in test_structured_tool.py around the
existing CrewStructuredTool.format_output_for_agent cases that returns a dict
with an unsupported key type so serialization raises, then assert the fallback
to str(...) is used for the result. Use the existing test helpers and
CrewStructuredTool.from_function/invoke/format_output_for_agent flow to locate
the right branch.
lib/crewai/src/crewai/tools/tool_usage.py (1)

714-718: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add direct tests for _format_result dict/list serialization path.

This branch changed behavior but isn’t directly covered by the new tests (which only exercise CrewStructuredTool.format_output_for_agent). Please add a focused unit test for _format_result with dict/list inputs (including fallback cases) to lock this path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/crewai/src/crewai/tools/tool_usage.py` around lines 714 - 718, Add
focused unit tests directly for the `_format_result` branch in `tool_usage.py`
that handles `dict` and `list` inputs, rather than only testing
`CrewStructuredTool.format_output_for_agent`. Cover both the successful
`json.dumps(..., ensure_ascii=False, default=str)` serialization path and the
fallback `str(result)` path when serialization raises `TypeError` or
`ValueError`, using `_format_result` as the target symbol so the behavior is
locked down independently.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/crewai/src/crewai/agents/crew_agent_executor.py`:
- Around line 244-247: The async feedback flow in CrewAgentExecutor still skips
the pre-feedback result display when verbose is false, unlike the sync invoke
path. Mirror the same visibility check and call to _show_logs(formatted_answer)
inside ainvoke before any feedback prompt, using the existing verbose condition
on self.agent.verbose and self.crew.verbose so both invoke and ainvoke behave
consistently.

---

Nitpick comments:
In `@lib/crewai/src/crewai/tools/tool_usage.py`:
- Around line 714-718: Add focused unit tests directly for the `_format_result`
branch in `tool_usage.py` that handles `dict` and `list` inputs, rather than
only testing `CrewStructuredTool.format_output_for_agent`. Cover both the
successful `json.dumps(..., ensure_ascii=False, default=str)` serialization path
and the fallback `str(result)` path when serialization raises `TypeError` or
`ValueError`, using `_format_result` as the target symbol so the behavior is
locked down independently.

In `@lib/crewai/tests/tools/test_structured_tool.py`:
- Around line 527-582: The new structured tool output tests cover JSON
serialization for dicts, lists, and strings, but they do not exercise the
fallback path when json.dumps(..., default=str) still fails. Add a test in
test_structured_tool.py around the existing
CrewStructuredTool.format_output_for_agent cases that returns a dict with an
unsupported key type so serialization raises, then assert the fallback to
str(...) is used for the result. Use the existing test helpers and
CrewStructuredTool.from_function/invoke/format_output_for_agent flow to locate
the right branch.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6556fb63-a45c-4ae8-b336-02589106aeab

📥 Commits

Reviewing files that changed from the base of the PR and between 01fc389 and 77be303.

📒 Files selected for processing (4)
  • lib/crewai/src/crewai/agents/crew_agent_executor.py
  • lib/crewai/src/crewai/tools/structured_tool.py
  • lib/crewai/src/crewai/tools/tool_usage.py
  • lib/crewai/tests/tools/test_structured_tool.py

Comment thread lib/crewai/src/crewai/agents/crew_agent_executor.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
lib/crewai/tests/tools/test_structured_tool.py (1)

602-604: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the fallback contract directly.

assert "value" in formatted still passes for several non-fallback outputs, so it doesn't actually lock in the str(raw_result) branch this test is named after. Comparing against str(result) or asserting that json.loads(formatted) fails would make this regression-proof.

Suggested test tightening
         result = tool.invoke(input={"query": "test"})
         formatted = tool.format_output_for_agent(result)
         assert isinstance(formatted, str)
-        assert "value" in formatted
+        assert formatted == str(result)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/crewai/tests/tools/test_structured_tool.py` around lines 602 - 604, The
structured tool fallback test is too permissive and does not verify the actual
fallback path in format_output_for_agent. Tighten the assertion in
test_structured_tool by comparing the formatted result directly to str(result),
or by explicitly asserting that JSON parsing fails for this output, so the test
uniquely covers the raw string fallback branch instead of any output containing
"value".
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/crewai/tests/tools/test_structured_tool.py`:
- Around line 584-640: The new structured-tool tests cover output serialization,
but ToolUsage._format_result is a separate formatting path that still needs
coverage. Add a companion unit test in test_tool_usage.py that exercises
ToolUsage._format_result with a tool result that is a dict or list and verifies
the returned value is valid JSON. Use the existing ToolUsage test setup/helpers
in test_tool_usage.py to locate the right path and ensure the assertion checks
the serialized output, not just argument handling.

---

Nitpick comments:
In `@lib/crewai/tests/tools/test_structured_tool.py`:
- Around line 602-604: The structured tool fallback test is too permissive and
does not verify the actual fallback path in format_output_for_agent. Tighten the
assertion in test_structured_tool by comparing the formatted result directly to
str(result), or by explicitly asserting that JSON parsing fails for this output,
so the test uniquely covers the raw string fallback branch instead of any output
containing "value".
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0caf6bd9-0242-4fcf-ba68-761e682f72d2

📥 Commits

Reviewing files that changed from the base of the PR and between 77be303 and 124c1bf.

📒 Files selected for processing (2)
  • lib/crewai/src/crewai/agents/crew_agent_executor.py
  • lib/crewai/tests/tools/test_structured_tool.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • lib/crewai/src/crewai/agents/crew_agent_executor.py

Comment on lines +584 to +640
def test_dict_with_unserializable_key_falls_back_to_str(self):
"""Dict with non-string keys that json.dumps(default=str) can't handle falls back to str()."""
from crewai.tools.structured_tool import CrewStructuredTool

class UnhashableKey:
"""A key type that json.dumps cannot serialize even with default=str."""
def __str__(self):
raise RuntimeError("cannot stringify")

def bad_key_tool(query: str) -> dict:
return {UnhashableKey(): "value"}

tool = CrewStructuredTool.from_function(
func=bad_key_tool,
name="bad_key_tool",
description="Returns a dict with unhashable keys.",
)
result = tool.invoke(input={"query": "test"})
formatted = tool.format_output_for_agent(result)
assert isinstance(formatted, str)
assert "value" in formatted

def test_dict_with_non_string_keys_serialized(self):
"""Dict with int keys should be JSON-serialized with default=str."""
import json
from crewai.tools.structured_tool import CrewStructuredTool

def int_key_tool(query: str) -> dict:
return {1: "one", 2: "two"}

tool = CrewStructuredTool.from_function(
func=int_key_tool,
name="int_key_tool",
description="Returns a dict with int keys.",
)
result = tool.invoke(input={"query": "test"})
formatted = tool.format_output_for_agent(result)
parsed = json.loads(formatted)
assert parsed["1"] == "one"

def test_nested_list_output_serialized(self):
"""Deeply nested list should be JSON-serialized."""
import json
from crewai.tools.structured_tool import CrewStructuredTool

def nested_list_tool(query: str) -> list:
return [[{"a": 1}], [{"b": 2}]]

tool = CrewStructuredTool.from_function(
func=nested_list_tool,
name="nested_list_tool",
description="Returns nested lists.",
)
result = tool.invoke(input={"query": "test"})
formatted = tool.format_output_for_agent(result)
parsed = json.loads(formatted)
assert parsed[0][0]["a"] == 1

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "ToolUsage serialization implementation:"
sed -n '709,720p' lib/crewai/src/crewai/tools/tool_usage.py

echo
echo "Tests that appear to cover ToolUsage/_format_result:"
rg -n -C2 --type=py '\b_format_result\s*\(|\bToolUsage\b' lib/crewai/tests

echo
echo "Structured tool serialization tests in this PR:"
rg -n -C2 --type=py '\bformat_output_for_agent\s*\(' lib/crewai/tests/tools/test_structured_tool.py

Repository: crewAIInc/crewAI

Length of output: 14295


Add companion unit test for ToolUsage._format_result

The new tests in test_structured_tool.py confirm CrewStructuredTool.format_output_for_agent handles serialization correctly. However, ToolUsage._format_result in lib/crewai/src/crewai/tools/tool_usage.py is a distinct code path responsible for formatting tool results during agent execution. The existing test_tool_usage.py focuses on input validation and argument parsing, lacking a test that asserts the output is JSON-serialized when a tool returns a dict or list.

Please add a test in lib/crewai/tests/tools/test_tool_usage.py that calls ToolUsage._format_result with a dict or list and asserts the return value is valid JSON.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/crewai/tests/tools/test_structured_tool.py` around lines 584 - 640, The
new structured-tool tests cover output serialization, but
ToolUsage._format_result is a separate formatting path that still needs
coverage. Add a companion unit test in test_tool_usage.py that exercises
ToolUsage._format_result with a tool result that is a dict or list and verifies
the returned value is valid JSON. Use the existing ToolUsage test setup/helpers
in test_tool_usage.py to locate the right path and ensure the assertion checks
the serialized output, not just argument handling.

@itxaiohanglover

Copy link
Copy Markdown

Nice fix! The JSON serialization with default=str fallback is the right approach — handles edge cases like datetime objects inside dicts. The test coverage for both dict and list outputs is solid.

One thought: the same serialization logic is duplicated in both structured_tool.py and tool_usage.py. Might be worth extracting to a shared helper, but not a blocker.

When a custom tool returns a dict or list, the output was being converted
using str() which produces Python repr format (e.g., single quotes, True/False).
This is not valid JSON and can cause parsing errors in the LLM.

Now uses json.dumps() for dict/list types, falling back to str() for
non-serializable types.

Fixes crewAIInc#6267
@C1-BA-B1-F3 C1-BA-B1-F3 force-pushed the fix/nested-dict-tool-output branch from 124c1bf to 20f90a3 Compare June 26, 2026 18:43

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
lib/crewai/src/crewai/tools/structured_tool.py (1)

73-79: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Update the warning text to match the new fallback.

Line 78 still says this path falls back to str(raw_result), but Line 83 now uses _serialize_to_json_string(raw_result). That warning will be misleading when debugging validation failures.

Proposed fix
         warnings.warn(
             (
                 f"Failed to validate or serialize output from tool "
                 f"'{getattr(tool, 'name', '<unknown>')}' using result_schema "
                 f"'{result_schema.__name__}': {exc.__class__.__name__}. "
-                "Falling back to str(raw_result)."
+                "Falling back to JSON serialization when possible, otherwise str(raw_result)."
             ),
             RuntimeWarning,
             stacklevel=2,
         )

Also applies to: 83-83

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/crewai/src/crewai/tools/structured_tool.py` around lines 73 - 79, The
warning message in the structured tool fallback path is out of sync with the
actual behavior. Update the warning text in structured_tool.py near the
validation/serialization failure handling so it references the JSON
serialization fallback used by the code path (through _serialize_to_json_string
in the structured tool logic) instead of saying it falls back to
str(raw_result). Keep the message aligned with the tool name and result_schema
context so debugging remains accurate.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@lib/crewai/src/crewai/tools/structured_tool.py`:
- Around line 73-79: The warning message in the structured tool fallback path is
out of sync with the actual behavior. Update the warning text in
structured_tool.py near the validation/serialization failure handling so it
references the JSON serialization fallback used by the code path (through
_serialize_to_json_string in the structured tool logic) instead of saying it
falls back to str(raw_result). Keep the message aligned with the tool name and
result_schema context so debugging remains accurate.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c2c6a02c-ccd4-47b7-965a-98c72f9976ee

📥 Commits

Reviewing files that changed from the base of the PR and between 124c1bf and 20f90a3.

📒 Files selected for processing (4)
  • lib/crewai/src/crewai/tools/structured_tool.py
  • lib/crewai/src/crewai/utilities/agent_utils.py
  • lib/crewai/tests/tools/test_base_tool.py
  • lib/crewai/tests/tools/test_structured_tool.py

@C1-BA-B1-F3

Copy link
Copy Markdown
Author

@itxaiohanglover Thanks for the kind words and the thoughtful suggestion! You're right that the serialization logic is duplicated — extracting it to a shared helper (e.g. in agent_utils.py) would be cleaner. I'll look into refactoring that in a follow-up if the maintainers agree it's worth the churn.

Appreciate the review!

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

Labels

None yet

Projects

None yet

2 participants