Skip to content

fix(python): serialize datetime/UUID/Decimal/Enum/set/dataclass in plain-dict tool results - #2246

Closed
rinceyuan wants to merge 1 commit into
github:mainfrom
rinceyuan:fix/python-tool-result-serialization
Closed

fix(python): serialize datetime/UUID/Decimal/Enum/set/dataclass in plain-dict tool results#2246
rinceyuan wants to merge 1 commit into
github:mainfrom
rinceyuan:fix/python-tool-result-serialization

Conversation

@rinceyuan

Copy link
Copy Markdown
Contributor

Summary

Fix silent tool failures when a @define_tool handler returns a plain dict (non-Pydantic) containing datetime, UUID, Decimal, Enum, set, or dataclass values.

Problem

_normalize_result uses json.dumps with a default hook that only handles BaseModel. Any other non-JSON-native type raises TypeError, which wrapped_handler catches and reports to the model as:

Invoking this tool produced an error. Detailed information is not available.

This is easy to hit (returning a dict with a timestamp or UUID) and hard to diagnose (the error is intentionally redacted).

Fix

Add type-specific fallbacks in default():

Type Serialization
datetime / date / time .isoformat()
UUID / Decimal str()
Enum .value
set / frozenset list()
dataclass dataclasses.asdict()

Tests

New test test_dict_with_non_primitive_values_is_serialized covers all 9 types in a single plain-dict return. Existing test_pydantic_model_with_non_primitive_fields_is_serialized continues to pass (Pydantic path uses model_dump(mode=json)).

Partially addresses #2203.

…result serialization

Plain dicts (non-Pydantic) containing these types raise TypeError in
json.dumps, causing the tool result to be silently reported as a failure
to the model.

Add type-specific fallbacks in _normalize_result's default() hook:
- datetime/date/time -> isoformat()
- UUID/Decimal -> str()
- Enum -> .value
- set/frozenset -> list()
- dataclass -> dataclasses.asdict()

Partially addresses github#2203.
@rinceyuan
rinceyuan requested a review from a team as a code owner August 4, 2026 02:23
@SteveSandersonMS

Copy link
Copy Markdown
Contributor

Thanks for investigating this. I don’t think broadening Python’s serializer is the right direction.

The legitimate Pydantic case is already handled by model_dump(mode="json"). Plain dictionaries containing non-JSON-native values should fail rather than gain Python-specific implicit conversions.

Tool calls can fail for many reasons—handler exceptions, invalid arguments, permission failures, timeouts, and serialization errors. Applications already need to display or log these failures. The current behavior fits that model: the precise reason is available in the persisted and live tool.execution_complete.error.message, while the model-facing message remains intentionally redacted.

This PR instead turns an intentional failure into success and expands the API contract without a product requirement. I’m closing it; we can separately improve documentation if the standard failure-observation path is unclear.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants