Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 11 additions & 6 deletions src/google/adk/telemetry/tracing.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,19 +109,24 @@
def _safe_json_serialize(obj) -> str:
"""Convert any Python object to a JSON-serializable type or string.

Handles Pydantic BaseModel instances (common as tool return types)
by calling model_dump() before JSON encoding.

Args:
obj: The object to serialize.

Returns:
The JSON-serialized object string or <non-serializable> if the object cannot be serialized.
The JSON-serialized object string or <not serializable> if the object
cannot be serialized.
"""
def _default(o: Any) -> Any:
if isinstance(o, BaseModel):
return o.model_dump(mode='json')
return '<not serializable>'

try:
# Try direct JSON serialization first
return json.dumps(
obj, ensure_ascii=False, default=lambda o: '<not serializable>'
)
except (TypeError, OverflowError):
return json.dumps(obj, ensure_ascii=False, default=_default)
except (TypeError, OverflowError, ValueError):
return '<not serializable>'


Expand Down
55 changes: 55 additions & 0 deletions tests/unittests/telemetry/test_spans.py
Original file line number Diff line number Diff line change
Expand Up @@ -1175,3 +1175,58 @@ async def test_generate_content_span_with_experimental_semconv(
assert attributes[GEN_AI_AGENT_NAME] == invocation_context.agent.name
assert GEN_AI_CONVERSATION_ID in attributes
assert attributes[GEN_AI_CONVERSATION_ID] == invocation_context.session.id


# ---------------------------------------------------------------------------
# _safe_json_serialize tests
# ---------------------------------------------------------------------------

from google.adk.telemetry.tracing import _safe_json_serialize
from pydantic import BaseModel as PydanticBaseModel


class _SampleToolResult(PydanticBaseModel):
query: str
total: int
items: list[str] = []


class _NestedModel(PydanticBaseModel):
inner: _SampleToolResult


def test_safe_json_serialize_plain_dict():
"""Plain dicts serialize normally."""
result = _safe_json_serialize({'key': 'value', 'num': 42})
assert json.loads(result) == {'key': 'value', 'num': 42}


def test_safe_json_serialize_pydantic_model_in_dict():
"""Pydantic models nested in a dict are serialized via model_dump."""
model = _SampleToolResult(query='test', total=2, items=['a', 'b'])
result = _safe_json_serialize({'result': model})
parsed = json.loads(result)
assert parsed == {'result': {'query': 'test', 'total': 2, 'items': ['a', 'b']}}


def test_safe_json_serialize_nested_pydantic_model():
"""Nested Pydantic models are fully serialized."""
inner = _SampleToolResult(query='q', total=0, items=[])
outer = _NestedModel(inner=inner)
result = _safe_json_serialize({'result': outer})
parsed = json.loads(result)
assert parsed['result']['inner'] == {'query': 'q', 'total': 0, 'items': []}


def test_safe_json_serialize_top_level_pydantic_model():
"""A top-level Pydantic model (not wrapped in a dict) is serialized."""
model = _SampleToolResult(query='direct', total=1, items=['x'])
result = _safe_json_serialize(model)
parsed = json.loads(result)
assert parsed == {'query': 'direct', 'total': 1, 'items': ['x']}


def test_safe_json_serialize_non_serializable_fallback():
"""Objects that are neither JSON-native nor Pydantic fall back gracefully."""
result = _safe_json_serialize({'value': object()})
assert '<not serializable>' in result