Skip to content

DatabaseSessionService.get_session() crashes with ValidationError when a transcription field is stored as the string "null" #6348

Description

@vas610

🔴 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:

  1. Install google-adk==1.28.1 (also confirmed present, unfixed, through 2.1.0 — see Regression section)
  2. Run the following minimal repro:
    from google.adk.sessions import _session_util
    from google.genai import types
    _session_util.decode_model("null", types.Transcription)
    
  3. 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)

Metadata

Metadata

Labels

request clarification[Status] The maintainer need clarification or more information from the authorservices[Component] This issue is related to runtime services, e.g. sessions, memory, artifacts, etc

Type

Fields

No fields configured for Bug.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions