From 79d9a84a11634e1ac1be68aa4214f473f747de77 Mon Sep 17 00:00:00 2001 From: Yizuki_Ame Date: Tue, 7 Apr 2026 02:57:50 +0800 Subject: [PATCH 1/2] Fix A2A metadata round-trip serialization Signed-off-by: Yizuki_Ame --- .../adk/a2a/converters/from_adk_event.py | 9 ++++ .../a2a/converters/test_event_round_trip.py | 42 +++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/src/google/adk/a2a/converters/from_adk_event.py b/src/google/adk/a2a/converters/from_adk_event.py index d64c1940fa..8d8f68bf40 100644 --- a/src/google/adk/a2a/converters/from_adk_event.py +++ b/src/google/adk/a2a/converters/from_adk_event.py @@ -17,6 +17,7 @@ from collections.abc import Callable from datetime import datetime from datetime import timezone +import json import logging from typing import Any from typing import Dict @@ -266,6 +267,14 @@ def _serialize_value(value: Any) -> Optional[Any]: logger.warning("Failed to serialize Pydantic model, falling back: %s", e) return str(value) + if isinstance(value, (dict, list, str, bool, int, float)): + return value + + try: + return json.loads(json.dumps(value)) + except (TypeError, ValueError): + pass + return str(value) diff --git a/tests/unittests/a2a/converters/test_event_round_trip.py b/tests/unittests/a2a/converters/test_event_round_trip.py index 00036f6af7..2c50015bfc 100644 --- a/tests/unittests/a2a/converters/test_event_round_trip.py +++ b/tests/unittests/a2a/converters/test_event_round_trip.py @@ -23,8 +23,10 @@ from a2a.types import TaskStatusUpdateEvent from google.adk.a2a.converters.from_adk_event import convert_event_to_a2a_events from google.adk.a2a.converters.from_adk_event import create_error_status_event +from google.adk.a2a.converters.to_adk_event import _parse_adk_metadata_value from google.adk.a2a.converters.to_adk_event import convert_a2a_artifact_update_to_event from google.adk.a2a.converters.to_adk_event import convert_a2a_status_update_to_event +from google.adk.a2a.converters.utils import _get_adk_metadata_key from google.adk.agents.invocation_context import InvocationContext from google.adk.events.event import Event from google.genai import types as genai_types @@ -206,3 +208,43 @@ def test_round_trip_function_response_event(): assert restored_event.content.parts[0].function_response.response == { "result": "success" } + + +def test_round_trip_custom_metadata_preserves_structured_values(): + original_custom_metadata = { + "flag": True, + "count": 42, + "nested": {"key": "val"}, + "tags": ["a", "b"], + } + original_event = Event( + invocation_id="test_invocation", + author="test_agent", + branch="main", + content=genai_types.Content( + role="model", + parts=[genai_types.Part.from_text(text="Hello world!")], + ), + custom_metadata=original_custom_metadata, + ) + agents_artifacts: Dict[str, str] = {} + + a2a_events = convert_event_to_a2a_events( + event=original_event, + agents_artifacts=agents_artifacts, + task_id="task1", + context_id="context1", + ) + + assert len(a2a_events) == 1 + a2a_event = a2a_events[0] + assert isinstance(a2a_event, TaskArtifactUpdateEvent) + + serialized_metadata = a2a_event.artifact.metadata[ + _get_adk_metadata_key("custom_metadata") + ] + + assert not isinstance(serialized_metadata, str) + assert ( + _parse_adk_metadata_value(serialized_metadata) == original_custom_metadata + ) From 0aacc2b7972aeb44c66fdbf31727fa06a1ca6d32 Mon Sep 17 00:00:00 2001 From: Yizuki_Ame Date: Fri, 10 Apr 2026 03:22:00 +0800 Subject: [PATCH 2/2] 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 --- .../adk/a2a/converters/from_adk_event.py | 17 +++++++- .../a2a/converters/test_event_round_trip.py | 40 +++++++++++++++++++ 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/src/google/adk/a2a/converters/from_adk_event.py b/src/google/adk/a2a/converters/from_adk_event.py index 8d8f68bf40..b5bb260014 100644 --- a/src/google/adk/a2a/converters/from_adk_event.py +++ b/src/google/adk/a2a/converters/from_adk_event.py @@ -267,11 +267,24 @@ def _serialize_value(value: Any) -> Optional[Any]: logger.warning("Failed to serialize Pydantic model, falling back: %s", e) return str(value) - if isinstance(value, (dict, list, str, bool, int, float)): + # JSON-native leaf types — return as-is + if isinstance(value, (str, bool, int, float)): return value + # Containers — recurse so nested non-serializable values are handled + if isinstance(value, dict): + return {str(k): _serialize_value(v) for k, v in value.items()} + + if isinstance(value, (list, tuple)): + return [_serialize_value(item) for item in value] + + # Common Python types with no JSON equivalent + if isinstance(value, (set, frozenset)): + return [_serialize_value(item) for item in sorted(value, key=str)] + + # Other objects — try JSON normalization, then str() fallback try: - return json.loads(json.dumps(value)) + return json.loads(json.dumps(value, default=str)) except (TypeError, ValueError): pass diff --git a/tests/unittests/a2a/converters/test_event_round_trip.py b/tests/unittests/a2a/converters/test_event_round_trip.py index 2c50015bfc..40a7d8c1d7 100644 --- a/tests/unittests/a2a/converters/test_event_round_trip.py +++ b/tests/unittests/a2a/converters/test_event_round_trip.py @@ -248,3 +248,43 @@ def test_round_trip_custom_metadata_preserves_structured_values(): assert ( _parse_adk_metadata_value(serialized_metadata) == original_custom_metadata ) + + +def test_serialize_value_handles_non_serializable_nested_types(): + """Regression: non-JSON-native types inside dicts/lists must not crash.""" + from datetime import datetime + from datetime import timezone + + from google.adk.a2a.converters.from_adk_event import _serialize_value + + ts = datetime(2026, 1, 15, 12, 0, 0, tzinfo=timezone.utc) + value = { + "created_at": ts, + "tags": {"alpha", "beta"}, + "normal": 42, + "nested_list": [True, ts], + } + + result = _serialize_value(value) + + # Result must be fully JSON-serializable (no crash) + import json + + json_str = json.dumps(result) + parsed = json.loads(json_str) + + # Leaf types preserved + assert parsed["normal"] == 42 + + # datetime falls back to str representation + assert isinstance(parsed["created_at"], str) + assert "2026" in parsed["created_at"] + + # set becomes a sorted list of strings + assert isinstance(parsed["tags"], list) + assert set(parsed["tags"]) == {"alpha", "beta"} + + # nested list with mixed types + assert parsed["nested_list"][0] is True + assert isinstance(parsed["nested_list"][1], str) +