Skip to content

Commit 732687d

Browse files
committed
fix: recursive serialization for nested non-JSON-native types
Address review feedback: _serialize_value() now recurses into dict/list values so that nested non-serializable types (datetime, set, etc.) are gracefully handled instead of being passed through raw. - str/bool/int/float: return as-is (JSON leaf types) - dict: recurse into values, coerce keys to str - list/tuple: recurse into elements - set/frozenset: convert to sorted list, recurse - Other: try json.loads(json.dumps(value, default=str)) fallback - Added regression test for datetime and set inside custom_metadata
1 parent 7ef9c09 commit 732687d

File tree

2 files changed

+55
-2
lines changed

2 files changed

+55
-2
lines changed

src/google/adk/a2a/converters/from_adk_event.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -250,11 +250,24 @@ def _serialize_value(value: Any) -> Optional[Any]:
250250
logger.warning("Failed to serialize Pydantic model, falling back: %s", e)
251251
return str(value)
252252

253-
if isinstance(value, (dict, list, str, bool, int, float)):
253+
# JSON-native leaf types — return as-is
254+
if isinstance(value, (str, bool, int, float)):
254255
return value
255256

257+
# Containers — recurse so nested non-serializable values are handled
258+
if isinstance(value, dict):
259+
return {str(k): _serialize_value(v) for k, v in value.items()}
260+
261+
if isinstance(value, (list, tuple)):
262+
return [_serialize_value(item) for item in value]
263+
264+
# Common Python types with no JSON equivalent
265+
if isinstance(value, (set, frozenset)):
266+
return [_serialize_value(item) for item in sorted(value, key=str)]
267+
268+
# Other objects — try JSON normalization, then str() fallback
256269
try:
257-
return json.loads(json.dumps(value))
270+
return json.loads(json.dumps(value, default=str))
258271
except (TypeError, ValueError):
259272
pass
260273

tests/unittests/a2a/converters/test_event_round_trip.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,3 +248,43 @@ def test_round_trip_custom_metadata_preserves_structured_values():
248248
assert (
249249
_parse_adk_metadata_value(serialized_metadata) == original_custom_metadata
250250
)
251+
252+
253+
def test_serialize_value_handles_non_serializable_nested_types():
254+
"""Regression: non-JSON-native types inside dicts/lists must not crash."""
255+
from datetime import datetime
256+
from datetime import timezone
257+
258+
from google.adk.a2a.converters.from_adk_event import _serialize_value
259+
260+
ts = datetime(2026, 1, 15, 12, 0, 0, tzinfo=timezone.utc)
261+
value = {
262+
"created_at": ts,
263+
"tags": {"alpha", "beta"},
264+
"normal": 42,
265+
"nested_list": [True, ts],
266+
}
267+
268+
result = _serialize_value(value)
269+
270+
# Result must be fully JSON-serializable (no crash)
271+
import json
272+
273+
json_str = json.dumps(result)
274+
parsed = json.loads(json_str)
275+
276+
# Leaf types preserved
277+
assert parsed["normal"] == 42
278+
279+
# datetime falls back to str representation
280+
assert isinstance(parsed["created_at"], str)
281+
assert "2026" in parsed["created_at"]
282+
283+
# set becomes a sorted list of strings
284+
assert isinstance(parsed["tags"], list)
285+
assert set(parsed["tags"]) == {"alpha", "beta"}
286+
287+
# nested list with mixed types
288+
assert parsed["nested_list"][0] is True
289+
assert isinstance(parsed["nested_list"][1], str)
290+

0 commit comments

Comments
 (0)