From 9f5a609fbec6aacba25894fa024116497aeaa28e Mon Sep 17 00:00:00 2001 From: Raman369AI Date: Fri, 6 Mar 2026 22:12:32 -0600 Subject: [PATCH 1/2] fix(sessions): prevent PydanticSerializationError when state contains non-serializable objects When Python callables or other non-JSON-serializable values are stored in session state (e.g. via MCP tool callbacks), EventActions.state_delta and agent_state would cause DatabaseSessionService.append_event to crash with PydanticSerializationError during event.model_dump(mode="json"). Add field serializers for state_delta and agent_state that recursively convert non-serializable leaf values to a descriptive string, allowing the event to be persisted without data loss for serializable fields. Fixes #4724 --- src/google/adk/events/event_actions.py | 32 ++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/google/adk/events/event_actions.py b/src/google/adk/events/event_actions.py index cfa73324b5..efb2a887d5 100644 --- a/src/google/adk/events/event_actions.py +++ b/src/google/adk/events/event_actions.py @@ -14,6 +14,7 @@ from __future__ import annotations +import json from typing import Any from typing import Optional @@ -22,12 +23,31 @@ from pydantic import BaseModel from pydantic import ConfigDict from pydantic import Field +from pydantic import field_serializer from ..auth.auth_tool import AuthConfig from ..tools.tool_confirmation import ToolConfirmation from .ui_widget import UiWidget +def _make_json_serializable(obj: Any) -> Any: + """Recursively converts an object to a JSON-serializable form. + + Non-serializable leaf values (e.g. Python callables stored in session state) + are replaced with a descriptive string so the overall structure can still be + persisted without crashing. + """ + if isinstance(obj, dict): + return {k: _make_json_serializable(v) for k, v in obj.items()} + if isinstance(obj, (list, tuple)): + return [_make_json_serializable(v) for v in obj] + try: + json.dumps(obj) + return obj + except (TypeError, ValueError): + return f'' + + class EventCompaction(BaseModel): """The compaction of the events.""" @@ -67,6 +87,10 @@ class EventActions(BaseModel): state_delta: dict[str, object] = Field(default_factory=dict) """Indicates that the event is updating the state with the given delta.""" + @field_serializer('state_delta', mode='plain') + def _serialize_state_delta(self, value: dict[str, object]) -> dict[str, Any]: + return _make_json_serializable(value) + artifact_delta: dict[str, int] = Field(default_factory=dict) """Indicates that the event is updating an artifact. key is the filename, value is the version.""" @@ -107,6 +131,14 @@ class EventActions(BaseModel): """The agent state at the current event, used for checkpoint and resume. This should only be set by ADK workflow.""" + @field_serializer('agent_state', mode='plain') + def _serialize_agent_state( + self, value: Optional[dict[str, Any]] + ) -> Optional[dict[str, Any]]: + if value is None: + return None + return _make_json_serializable(value) + rewind_before_invocation_id: Optional[str] = None """The invocation id to rewind to. This is only set for rewind event.""" From f2e73e9bc1caf7c65adfac708cf7620c95fc5c32 Mon Sep 17 00:00:00 2001 From: Raman369AI Date: Tue, 17 Mar 2026 20:21:56 -0500 Subject: [PATCH 2/2] fix(events): fix mypy errors in EventActions field serializers - Add cast(dict[str, Any], ...) to _serialize_state_delta and _serialize_agent_state return values to resolve no-any-return errors - Add # type: ignore[misc] to @field_serializer decorators to suppress untyped-decorator errors (known pydantic/mypy compatibility issue) - Add # type: ignore[misc] to EventCompaction and EventActions class definitions to suppress pre-existing BaseModel subclass typing errors --- src/google/adk/events/event_actions.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/google/adk/events/event_actions.py b/src/google/adk/events/event_actions.py index efb2a887d5..157fe535c9 100644 --- a/src/google/adk/events/event_actions.py +++ b/src/google/adk/events/event_actions.py @@ -16,6 +16,7 @@ import json from typing import Any +from typing import cast from typing import Optional from google.genai.types import Content @@ -48,7 +49,7 @@ def _make_json_serializable(obj: Any) -> Any: return f'' -class EventCompaction(BaseModel): +class EventCompaction(BaseModel): # type: ignore[misc] """The compaction of the events.""" model_config = ConfigDict( @@ -68,7 +69,7 @@ class EventCompaction(BaseModel): """The compacted content of the events.""" -class EventActions(BaseModel): +class EventActions(BaseModel): # type: ignore[misc] """Represents the actions attached to an event.""" model_config = ConfigDict( @@ -87,9 +88,9 @@ class EventActions(BaseModel): state_delta: dict[str, object] = Field(default_factory=dict) """Indicates that the event is updating the state with the given delta.""" - @field_serializer('state_delta', mode='plain') + @field_serializer('state_delta', mode='plain') # type: ignore[misc, untyped-decorator] def _serialize_state_delta(self, value: dict[str, object]) -> dict[str, Any]: - return _make_json_serializable(value) + return cast(dict[str, Any], _make_json_serializable(value)) artifact_delta: dict[str, int] = Field(default_factory=dict) """Indicates that the event is updating an artifact. key is the filename, @@ -131,13 +132,13 @@ def _serialize_state_delta(self, value: dict[str, object]) -> dict[str, Any]: """The agent state at the current event, used for checkpoint and resume. This should only be set by ADK workflow.""" - @field_serializer('agent_state', mode='plain') + @field_serializer('agent_state', mode='plain') # type: ignore[misc, untyped-decorator] def _serialize_agent_state( self, value: Optional[dict[str, Any]] ) -> Optional[dict[str, Any]]: if value is None: return None - return _make_json_serializable(value) + return cast(dict[str, Any], _make_json_serializable(value)) rewind_before_invocation_id: Optional[str] = None """The invocation id to rewind to. This is only set for rewind event."""