Skip to content

Commit 60e919d

Browse files
committed
fix: handle Pydantic models in _safe_json_serialize for tool tracing
Tools returning Pydantic BaseModel instances have their trace output silently replaced with '<not serializable>' because json.dumps cannot serialize Pydantic models natively. Update the default handler to call model_dump(mode='json') on BaseModel instances before falling back, consistent with how other functions in the same module already handle Pydantic objects. Fixes #4629
1 parent 0b1cff2 commit 60e919d

File tree

2 files changed

+67
-5
lines changed

2 files changed

+67
-5
lines changed

src/google/adk/telemetry/tracing.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,18 +67,25 @@
6767
def _safe_json_serialize(obj) -> str:
6868
"""Convert any Python object to a JSON-serializable type or string.
6969
70+
Handles Pydantic BaseModel instances (common as tool return types)
71+
by calling model_dump() before JSON encoding.
72+
7073
Args:
7174
obj: The object to serialize.
7275
7376
Returns:
74-
The JSON-serialized object string or <non-serializable> if the object cannot be serialized.
77+
The JSON-serialized object string or <not serializable> if the object
78+
cannot be serialized.
7579
"""
80+
from pydantic import BaseModel
81+
82+
def _default(o: Any) -> Any:
83+
if isinstance(o, BaseModel):
84+
return o.model_dump(mode='json')
85+
return '<not serializable>'
7686

7787
try:
78-
# Try direct JSON serialization first
79-
return json.dumps(
80-
obj, ensure_ascii=False, default=lambda o: '<not serializable>'
81-
)
88+
return json.dumps(obj, ensure_ascii=False, default=_default)
8289
except (TypeError, OverflowError):
8390
return '<not serializable>'
8491

tests/unittests/telemetry/test_spans.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -517,3 +517,58 @@ def test_trace_merged_tool_disabling_request_response_content(
517517
"Attribute 'gcp.vertex.agent.tool_response' was incorrectly set on the"
518518
' span.'
519519
)
520+
521+
522+
# ---------------------------------------------------------------------------
523+
# _safe_json_serialize tests
524+
# ---------------------------------------------------------------------------
525+
526+
from google.adk.telemetry.tracing import _safe_json_serialize
527+
from pydantic import BaseModel as PydanticBaseModel
528+
529+
530+
class _SampleToolResult(PydanticBaseModel):
531+
query: str
532+
total: int
533+
items: list[str] = []
534+
535+
536+
class _NestedModel(PydanticBaseModel):
537+
inner: _SampleToolResult
538+
539+
540+
def test_safe_json_serialize_plain_dict():
541+
"""Plain dicts serialize normally."""
542+
result = _safe_json_serialize({'key': 'value', 'num': 42})
543+
assert json.loads(result) == {'key': 'value', 'num': 42}
544+
545+
546+
def test_safe_json_serialize_pydantic_model_in_dict():
547+
"""Pydantic models nested in a dict are serialized via model_dump."""
548+
model = _SampleToolResult(query='test', total=2, items=['a', 'b'])
549+
result = _safe_json_serialize({'result': model})
550+
parsed = json.loads(result)
551+
assert parsed == {'result': {'query': 'test', 'total': 2, 'items': ['a', 'b']}}
552+
553+
554+
def test_safe_json_serialize_nested_pydantic_model():
555+
"""Nested Pydantic models are fully serialized."""
556+
inner = _SampleToolResult(query='q', total=0, items=[])
557+
outer = _NestedModel(inner=inner)
558+
result = _safe_json_serialize({'result': outer})
559+
parsed = json.loads(result)
560+
assert parsed['result']['inner'] == {'query': 'q', 'total': 0, 'items': []}
561+
562+
563+
def test_safe_json_serialize_top_level_pydantic_model():
564+
"""A top-level Pydantic model (not wrapped in a dict) is serialized."""
565+
model = _SampleToolResult(query='direct', total=1, items=['x'])
566+
result = _safe_json_serialize(model)
567+
parsed = json.loads(result)
568+
assert parsed == {'query': 'direct', 'total': 1, 'items': ['x']}
569+
570+
571+
def test_safe_json_serialize_non_serializable_fallback():
572+
"""Objects that are neither JSON-native nor Pydantic fall back gracefully."""
573+
result = _safe_json_serialize({'value': object()})
574+
assert '<not serializable>' in result

0 commit comments

Comments
 (0)