🔴 Required Information
Describe the Bug:
DatabaseSessionService.get_session() crashes with a pydantic.ValidationError when replaying a session whose most recent event has a transcription field (input_transcription/output_transcription) persisted as the JSON string "null" instead of SQL
NULL/Python None. The root cause is in sessions/_session_util.py::decode_model():
def decode_model(data, model_cls):
if data is None:
return None
return model_cls.model_validate(data)
This only guards against data is None. If data is any other falsy-but-not-None value — in our case the string "null" — it's passed straight to model_cls.model_validate(data), which raises ValidationError because a bare string isn't a valid dict for a
pydantic model. The same unguarded pattern is used for every field decoded this way in schemas/v0.py::to_event() (content, grounding_metadata, usage_metadata, citation_metadata, input_transcription, output_transcription).
We hit this in production after a Cloud Run 502 (GFE-level, connection dropped mid-request) interrupted an in-flight /run call. On retry against the same session_id, get_session() tried to reload the session's events and hit this crash instead of
executing the agent — the retry failed in ~25ms with no actual agent work performed. We haven't isolated exactly how the corrupted event got the literal string "null" written into its input_transcription column (this occurred on the interrupted first run,
likely a race between the aborted request and ADK's background event-persistence coroutine) — but regardless of how that value got there, decode_model should not let a malformed row crash the entire session reload.
Steps to Reproduce:
- Install google-adk==1.28.1 (also confirmed present, unfixed, through 2.1.0 — see Regression section)
- Run the following minimal repro:
from google.adk.sessions import _session_util
from google.genai import types
_session_util.decode_model("null", types.Transcription)
- Observe the ValidationError below.
(To reproduce end-to-end via the actual code path: use DatabaseSessionService against any SQL backend, manually set an event row's input_transcription column to the JSON string "null" — e.g. UPDATE events SET input_transcription = '"null"' WHERE id = ...
on Postgres/JSONB — then call get_session() for that session.)
Expected Behavior:
get_session() should not fail outright on a single malformed event's transcription field. At minimum, decode_model should tolerate non-dict, non-None input by returning None (dropping the malformed field) and logging a warning, rather than raising and
aborting the entire session load. This is consistent with how partial/corrupt data is often handled elsewhere in the codebase (e.g. _truncate_str added in schemas/v0.py to defensively handle oversized error_message values rather than letting the DB write
crash).
Observed Behavior:
pydantic_core._pydantic_core.ValidationError: 1 validation error for Transcription
Input should be a valid dictionary or object to extract fields from [type=model_attributes_type, input_value='null', input_type=str]
For further information visit https://errors.pydantic.dev/2.13/v/model_attributes_type
Full traceback from our production incident (Cloud Run, google-adk==1.28.1):
Traceback (most recent call last):
File ".../starlette/middleware/base.py", line 168, in call_next
raise app_exc from app_exc.cause or app_exc.context
File ".../starlette/middleware/base.py", line 144, in coro
await self.app(scope, receive_or_disconnect, send_no_error)
File ".../google/adk/cli/adk_web_server.py", line 266, in call
await self._app(scope, receive, send)
...
File ".../fastapi/routing.py", line 337, in run_endpoint_function
return await dependant.call(**values)
File ".../google/adk/cli/adk_web_server.py", line 1829, in run_agent
events = [event async for event in agen]
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File ".../google/adk/runners.py", line 632, in run_async
async for event in agen:
File ".../google/adk/runners.py", line 547, in _run_with_trace
session = await self._get_or_create_session(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File ".../google/adk/runners.py", line 422, in _get_or_create_session
session = await self.session_service.get_session(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File ".../google/adk/sessions/database_session_service.py", line 545, in get_session
events = [e.to_event() for e in reversed(storage_events)]
^^^^^^^^^^^^
File ".../google/adk/sessions/schemas/v0.py", line 363, in to_event
input_transcription=_session_util.decode_model(
^^^^^^^^^^^^^^^^^^^^^^^^^^^
File ".../google/adk/sessions/_session_util.py", line 34, in decode_model
return model_cls.model_validate(data)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
pydantic_core._pydantic_core.ValidationError: 1 validation error for Transcription
Input should be a valid dictionary or object to extract fields from [type=model_attributes_type, input_value='null', input_type=str]
For further information visit https://errors.pydantic.dev/2.13/v/model_attributes_type
Cloud Run request log for the failed retry:
Request failed (unhandled exception): POST /run | status=500 duration_ms=25
error=1 validation error for Transcription
Input should be a valid dictionary or object to extract fields from [type=model_attributes_type, input_value='null', input_type=str]
Environment Details:
- ADK Library Version (pip show google-adk): 1.28.1 (also reproduced the underlying code defect, unpatched, through 2.1.0 — see Regression)
- Desktop OS: Linux (Cloud Run container, python:3.12-slim-based image)
- Python Version (python -V): 3.12
Model Information:
- Are you using LiteLLM: No
- Which model is being used: gemini-2.5-pro
🟡 Optional Information
Regression:
Not a regression — appears present in every version we checked. We diffed sessions/_session_util.py and sessions/schemas/v0.py across 1.28.1 (our prod version) through every intermediate minor release (1.29.0–1.36.0, 2.0.0, 2.1.0) and decode_model()'s
guard logic is byte-identical in all of them. The only related change in that window is an unrelated _truncate_str fix for oversized error_message values (schemas/v0.py), which doesn't touch transcription decoding.
Logs:
See stack traces above (Observed Behavior section) — captured from Cloud Run logging (resource.type="cloud_run_revision") at the time of the incident.
Additional Context:
This surfaced downstream in our Interface Controller as a secondary failure mode: after a Cloud Run/GFE 502 dropped the connection mid-/run (Google infra-level, not an app crash — the agent container kept running), our controller retried /run against the
same session_id (by design, to resume in-session state rather than start over). The retry hit this bug on session reload and failed immediately (500 in 25ms, no agent work performed), which is a materially worse failure mode than the original transient
502 — a corrupted session becomes permanently unusable until manually cleaned up, since every retry against that session_id will hit the same crash.
Minimal Reproduction Code:
from google.adk.sessions import _session_util
from google.genai import types
# Simulates a session-store row where a transcription field was
# somehow persisted as the JSON string "null" instead of SQL NULL.
_session_util.decode_model("null", types.Transcription)
# Raises: pydantic_core._pydantic_core.ValidationError
Verified this reproduces the exact same ValidationError message as our production incident.
How often has this issue occurred?:
- Once / Rare (twice in production so far, under light load — but permanently breaks the affected session_id once triggered, until removed from the session store)
🔴 Required Information
Describe the Bug:
DatabaseSessionService.get_session()crashes with apydantic.ValidationErrorwhen replaying a session whose most recent event has a transcription field (input_transcription/output_transcription) persisted as the JSON string"null"instead of SQLNULL/PythonNone. The root cause is insessions/_session_util.py::decode_model():This only guards against data is None. If data is any other falsy-but-not-None value — in our case the string "null" — it's passed straight to model_cls.model_validate(data), which raises ValidationError because a bare string isn't a valid dict for a
pydantic model. The same unguarded pattern is used for every field decoded this way in schemas/v0.py::to_event() (content, grounding_metadata, usage_metadata, citation_metadata, input_transcription, output_transcription).
We hit this in production after a Cloud Run 502 (GFE-level, connection dropped mid-request) interrupted an in-flight /run call. On retry against the same session_id, get_session() tried to reload the session's events and hit this crash instead of
executing the agent — the retry failed in ~25ms with no actual agent work performed. We haven't isolated exactly how the corrupted event got the literal string "null" written into its input_transcription column (this occurred on the interrupted first run,
likely a race between the aborted request and ADK's background event-persistence coroutine) — but regardless of how that value got there, decode_model should not let a malformed row crash the entire session reload.
Steps to Reproduce:
(To reproduce end-to-end via the actual code path: use DatabaseSessionService against any SQL backend, manually set an event row's input_transcription column to the JSON string "null" — e.g. UPDATE events SET input_transcription = '"null"' WHERE id = ...
on Postgres/JSONB — then call get_session() for that session.)
Expected Behavior:
get_session() should not fail outright on a single malformed event's transcription field. At minimum, decode_model should tolerate non-dict, non-None input by returning None (dropping the malformed field) and logging a warning, rather than raising and
aborting the entire session load. This is consistent with how partial/corrupt data is often handled elsewhere in the codebase (e.g. _truncate_str added in schemas/v0.py to defensively handle oversized error_message values rather than letting the DB write
crash).
Observed Behavior:
pydantic_core._pydantic_core.ValidationError: 1 validation error for Transcription
Input should be a valid dictionary or object to extract fields from [type=model_attributes_type, input_value='null', input_type=str]
For further information visit https://errors.pydantic.dev/2.13/v/model_attributes_type
Full traceback from our production incident (Cloud Run, google-adk==1.28.1):
Traceback (most recent call last):
File ".../starlette/middleware/base.py", line 168, in call_next
raise app_exc from app_exc.cause or app_exc.context
File ".../starlette/middleware/base.py", line 144, in coro
await self.app(scope, receive_or_disconnect, send_no_error)
File ".../google/adk/cli/adk_web_server.py", line 266, in call
await self._app(scope, receive, send)
...
File ".../fastapi/routing.py", line 337, in run_endpoint_function
return await dependant.call(**values)
File ".../google/adk/cli/adk_web_server.py", line 1829, in run_agent
events = [event async for event in agen]
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File ".../google/adk/runners.py", line 632, in run_async
async for event in agen:
File ".../google/adk/runners.py", line 547, in _run_with_trace
session = await self._get_or_create_session(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File ".../google/adk/runners.py", line 422, in _get_or_create_session
session = await self.session_service.get_session(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File ".../google/adk/sessions/database_session_service.py", line 545, in get_session
events = [e.to_event() for e in reversed(storage_events)]
^^^^^^^^^^^^
File ".../google/adk/sessions/schemas/v0.py", line 363, in to_event
input_transcription=_session_util.decode_model(
^^^^^^^^^^^^^^^^^^^^^^^^^^^
File ".../google/adk/sessions/_session_util.py", line 34, in decode_model
return model_cls.model_validate(data)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
pydantic_core._pydantic_core.ValidationError: 1 validation error for Transcription
Input should be a valid dictionary or object to extract fields from [type=model_attributes_type, input_value='null', input_type=str]
For further information visit https://errors.pydantic.dev/2.13/v/model_attributes_type
Cloud Run request log for the failed retry:
Request failed (unhandled exception): POST /run | status=500 duration_ms=25
error=1 validation error for Transcription
Input should be a valid dictionary or object to extract fields from [type=model_attributes_type, input_value='null', input_type=str]
Environment Details:
Model Information:
🟡 Optional Information
Regression:
Not a regression — appears present in every version we checked. We diffed sessions/_session_util.py and sessions/schemas/v0.py across 1.28.1 (our prod version) through every intermediate minor release (1.29.0–1.36.0, 2.0.0, 2.1.0) and decode_model()'s
guard logic is byte-identical in all of them. The only related change in that window is an unrelated _truncate_str fix for oversized error_message values (schemas/v0.py), which doesn't touch transcription decoding.
Logs:
See stack traces above (Observed Behavior section) — captured from Cloud Run logging (resource.type="cloud_run_revision") at the time of the incident.
Additional Context:
This surfaced downstream in our Interface Controller as a secondary failure mode: after a Cloud Run/GFE 502 dropped the connection mid-/run (Google infra-level, not an app crash — the agent container kept running), our controller retried /run against the
same session_id (by design, to resume in-session state rather than start over). The retry hit this bug on session reload and failed immediately (500 in 25ms, no agent work performed), which is a materially worse failure mode than the original transient
502 — a corrupted session becomes permanently unusable until manually cleaned up, since every retry against that session_id will hit the same crash.
Minimal Reproduction Code:
How often has this issue occurred?: