Skip to content

Commit 9fe5652

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 1206add commit 9fe5652

File tree

2 files changed

+66
-6
lines changed

2 files changed

+66
-6
lines changed

src/google/adk/telemetry/tracing.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -109,19 +109,24 @@
109109
def _safe_json_serialize(obj) -> str:
110110
"""Convert any Python object to a JSON-serializable type or string.
111111
112+
Handles Pydantic BaseModel instances (common as tool return types)
113+
by calling model_dump() before JSON encoding.
114+
112115
Args:
113116
obj: The object to serialize.
114117
115118
Returns:
116-
The JSON-serialized object string or <non-serializable> if the object cannot be serialized.
119+
The JSON-serialized object string or <not serializable> if the object
120+
cannot be serialized.
117121
"""
122+
def _default(o: Any) -> Any:
123+
if isinstance(o, BaseModel):
124+
return o.model_dump(mode='json')
125+
return '<not serializable>'
118126

119127
try:
120-
# Try direct JSON serialization first
121-
return json.dumps(
122-
obj, ensure_ascii=False, default=lambda o: '<not serializable>'
123-
)
124-
except (TypeError, OverflowError):
128+
return json.dumps(obj, ensure_ascii=False, default=_default)
129+
except (TypeError, OverflowError, ValueError):
125130
return '<not serializable>'
126131

127132

tests/unittests/telemetry/test_spans.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1175,3 +1175,58 @@ async def test_generate_content_span_with_experimental_semconv(
11751175
assert attributes[GEN_AI_AGENT_NAME] == invocation_context.agent.name
11761176
assert GEN_AI_CONVERSATION_ID in attributes
11771177
assert attributes[GEN_AI_CONVERSATION_ID] == invocation_context.session.id
1178+
1179+
1180+
# ---------------------------------------------------------------------------
1181+
# _safe_json_serialize tests
1182+
# ---------------------------------------------------------------------------
1183+
1184+
from google.adk.telemetry.tracing import _safe_json_serialize
1185+
from pydantic import BaseModel as PydanticBaseModel
1186+
1187+
1188+
class _SampleToolResult(PydanticBaseModel):
1189+
query: str
1190+
total: int
1191+
items: list[str] = []
1192+
1193+
1194+
class _NestedModel(PydanticBaseModel):
1195+
inner: _SampleToolResult
1196+
1197+
1198+
def test_safe_json_serialize_plain_dict():
1199+
"""Plain dicts serialize normally."""
1200+
result = _safe_json_serialize({'key': 'value', 'num': 42})
1201+
assert json.loads(result) == {'key': 'value', 'num': 42}
1202+
1203+
1204+
def test_safe_json_serialize_pydantic_model_in_dict():
1205+
"""Pydantic models nested in a dict are serialized via model_dump."""
1206+
model = _SampleToolResult(query='test', total=2, items=['a', 'b'])
1207+
result = _safe_json_serialize({'result': model})
1208+
parsed = json.loads(result)
1209+
assert parsed == {'result': {'query': 'test', 'total': 2, 'items': ['a', 'b']}}
1210+
1211+
1212+
def test_safe_json_serialize_nested_pydantic_model():
1213+
"""Nested Pydantic models are fully serialized."""
1214+
inner = _SampleToolResult(query='q', total=0, items=[])
1215+
outer = _NestedModel(inner=inner)
1216+
result = _safe_json_serialize({'result': outer})
1217+
parsed = json.loads(result)
1218+
assert parsed['result']['inner'] == {'query': 'q', 'total': 0, 'items': []}
1219+
1220+
1221+
def test_safe_json_serialize_top_level_pydantic_model():
1222+
"""A top-level Pydantic model (not wrapped in a dict) is serialized."""
1223+
model = _SampleToolResult(query='direct', total=1, items=['x'])
1224+
result = _safe_json_serialize(model)
1225+
parsed = json.loads(result)
1226+
assert parsed == {'query': 'direct', 'total': 1, 'items': ['x']}
1227+
1228+
1229+
def test_safe_json_serialize_non_serializable_fallback():
1230+
"""Objects that are neither JSON-native nor Pydantic fall back gracefully."""
1231+
result = _safe_json_serialize({'value': object()})
1232+
assert '<not serializable>' in result

0 commit comments

Comments
 (0)