Skip to content

Commit 0aacc2b

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 79d9a84 commit 0aacc2b

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
@@ -267,11 +267,24 @@ def _serialize_value(value: Any) -> Optional[Any]:
267267
logger.warning("Failed to serialize Pydantic model, falling back: %s", e)
268268
return str(value)
269269

270-
if isinstance(value, (dict, list, str, bool, int, float)):
270+
# JSON-native leaf types — return as-is
271+
if isinstance(value, (str, bool, int, float)):
271272
return value
272273

274+
# Containers — recurse so nested non-serializable values are handled
275+
if isinstance(value, dict):
276+
return {str(k): _serialize_value(v) for k, v in value.items()}
277+
278+
if isinstance(value, (list, tuple)):
279+
return [_serialize_value(item) for item in value]
280+
281+
# Common Python types with no JSON equivalent
282+
if isinstance(value, (set, frozenset)):
283+
return [_serialize_value(item) for item in sorted(value, key=str)]
284+
285+
# Other objects — try JSON normalization, then str() fallback
273286
try:
274-
return json.loads(json.dumps(value))
287+
return json.loads(json.dumps(value, default=str))
275288
except (TypeError, ValueError):
276289
pass
277290

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)