π΄ Required Information
Describe the Bug:
BaseNode._validate_schema documents that validated BaseModel instances are converted to dicts "to keep
Event.output JSON-serializable", but the conversion calls model_dump() β pydantic's python mode.
Python mode preserves Decimal, datetime, UUID and non-str Enum members, and skips any serializer
registered with when_used="json". The resulting dict is therefore not JSON-serializable, which is the
opposite of the documented intent.
The docstring and the implementation contradict each other:
src/google/adk/workflow/_base_node.py
def _validate_schema(self, data: Any, schema: Any) -> Any:
"""... Any BaseModel instances in the validated
result are converted to dicts via model_dump() to keep
Event.output JSON-serializable.
"""
...
validated = TypeAdapter(schema).validate_python(data)
return self._to_serializable(validated)
@staticmethod
def _to_serializable(data: Any) -> Any:
"""Converts BaseModel instances to dicts recursively."""
if isinstance(data, BaseModel):
return data.model_dump() # <-- python mode
Both validation paths reach it: _validate_input_data β _validate_schema β _to_serializable, and
_validate_output_data β _validate_schema β _to_serializable, plus a direct call on the
JSON-reparse fallback.
Steps to Reproduce:
pip install google-adk==2.3.0
Save the snippet below as repro.py
Run python repro.py
It prints a dict containing Decimal('29.99') and then raises
TypeError: Object of type Decimal is not JSON serializable
import json
from decimal import Decimal
from typing import Annotated
from google.adk.workflow._base_node import BaseNode
from pydantic import BaseModel, PlainSerializer
JsonDecimal = Annotated[Decimal, PlainSerializer(float, return_type=float, when_used="json")]
class Price(BaseModel):
amount: JsonDecimal
class Payload(BaseModel):
price: Price
node = BaseNode(name="pricing", output_schema=Payload)
validated = node._validate_output_data({"price": {"amount": "29.99"}})
print(validated) # {'price': {'amount': Decimal('29.99')}}
json.dumps(validated) # TypeError: Object of type Decimal is not JSON serializable
The node is constructed with the public output_schema field and handed the plain dict a model would
return; _validate_output_data is the method ADK's own node runner invokes on that result.
Expected Behavior:
The validated result is JSON-serializable, as _validate_schema documents β the when_used="json"
serializer the schema declares should run, so amount becomes the number 29.99.
Observed Behavior:
{'price': {'amount': Decimal('29.99')}}
Traceback (most recent call last):
...
TypeError: Object of type Decimal is not JSON serializable
Environment Details:
ADK Library Version (pip show google-adk): 2.3.0 (also present in 2.7.0 β see Regression)
Desktop OS: Linux
Python Version (python -V): 3.12.3
Model Information:
Are you using LiteLLM: No
Which model is being used: N/A β this is a schema-validation defect in _base_node.py and reproduces
with no model call at all.
π‘ Optional Information
Regression:
Not a regression; the code path looks the same in 2.7.0, the latest release at time of writing.
Additional Context:
Decimal is the case we hit in production, but it is not special β any type pydantic renders differently
in JSON mode fails the same way. Two more, both of which json.dumps also rejects:
Object of type MyEnum is not JSON serializable # a plain Enum field (not StrEnum)
Object of type datetime is not JSON serializable # a datetime field
Suggested fix β one keyword, covering both validation paths and every nesting depth, since the
list/dict branches recurse back into the same function:
@staticmethod
def _to_serializable(data: Any) -> Any:
"""Converts BaseModel instances to dicts recursively."""
if isinstance(data, BaseModel):
-
return data.model_dump(mode="json")
Happy to send this as a PR if that is useful.
Our workaround is to patch _to_serializable to dump in JSON mode and delegate every non-BaseModel
value back to the original implementation. It is guarded by a test asserting the unpatched function still
shows the defect, so it fails loudly and can be removed once this is fixed upstream.
Minimal Reproduction Code: see Steps to Reproduce above.
How often has this issue occurred?: Always (100%) β deterministic for any schema containing such a field.
π΄ Required Information
Describe the Bug:
BaseNode._validate_schema documents that validated BaseModel instances are converted to dicts "to keep
Event.output JSON-serializable", but the conversion calls model_dump() β pydantic's python mode.
Python mode preserves Decimal, datetime, UUID and non-str Enum members, and skips any serializer
registered with when_used="json". The resulting dict is therefore not JSON-serializable, which is the
opposite of the documented intent.
The docstring and the implementation contradict each other:
src/google/adk/workflow/_base_node.py
def _validate_schema(self, data: Any, schema: Any) -> Any:
"""... Any BaseModel instances in the validated
result are converted to dicts via
model_dump()to keepEvent.outputJSON-serializable."""
...
validated = TypeAdapter(schema).validate_python(data)
return self._to_serializable(validated)
@staticmethod
def _to_serializable(data: Any) -> Any:
"""Converts BaseModel instances to dicts recursively."""
if isinstance(data, BaseModel):
return data.model_dump() # <-- python mode
Both validation paths reach it: _validate_input_data β _validate_schema β _to_serializable, and
_validate_output_data β _validate_schema β _to_serializable, plus a direct call on the
JSON-reparse fallback.
Steps to Reproduce:
pip install google-adk==2.3.0
Save the snippet below as repro.py
Run python repro.py
It prints a dict containing Decimal('29.99') and then raises
TypeError: Object of type Decimal is not JSON serializable
import json
from decimal import Decimal
from typing import Annotated
from google.adk.workflow._base_node import BaseNode
from pydantic import BaseModel, PlainSerializer
JsonDecimal = Annotated[Decimal, PlainSerializer(float, return_type=float, when_used="json")]
class Price(BaseModel):
amount: JsonDecimal
class Payload(BaseModel):
price: Price
node = BaseNode(name="pricing", output_schema=Payload)
validated = node._validate_output_data({"price": {"amount": "29.99"}})
print(validated) # {'price': {'amount': Decimal('29.99')}}
json.dumps(validated) # TypeError: Object of type Decimal is not JSON serializable
The node is constructed with the public output_schema field and handed the plain dict a model would
return; _validate_output_data is the method ADK's own node runner invokes on that result.
Expected Behavior:
The validated result is JSON-serializable, as _validate_schema documents β the when_used="json"
serializer the schema declares should run, so amount becomes the number 29.99.
Observed Behavior:
{'price': {'amount': Decimal('29.99')}}
Traceback (most recent call last):
...
TypeError: Object of type Decimal is not JSON serializable
Environment Details:
ADK Library Version (pip show google-adk): 2.3.0 (also present in 2.7.0 β see Regression)
Desktop OS: Linux
Python Version (python -V): 3.12.3
Model Information:
Are you using LiteLLM: No
Which model is being used: N/A β this is a schema-validation defect in _base_node.py and reproduces
with no model call at all.
π‘ Optional Information
Regression:
Not a regression; the code path looks the same in 2.7.0, the latest release at time of writing.
Additional Context:
Decimal is the case we hit in production, but it is not special β any type pydantic renders differently
in JSON mode fails the same way. Two more, both of which json.dumps also rejects:
Object of type MyEnum is not JSON serializable # a plain Enum field (not StrEnum)
Object of type datetime is not JSON serializable # a datetime field
Suggested fix β one keyword, covering both validation paths and every nesting depth, since the
list/dict branches recurse back into the same function:
@staticmethod
def _to_serializable(data: Any) -> Any:
"""Converts BaseModel instances to dicts recursively."""
if isinstance(data, BaseModel):
Happy to send this as a PR if that is useful.
Our workaround is to patch _to_serializable to dump in JSON mode and delegate every non-BaseModel
value back to the original implementation. It is guarded by a test asserting the unpatched function still
shows the defect, so it fails loudly and can be removed once this is fixed upstream.
Minimal Reproduction Code: see Steps to Reproduce above.
How often has this issue occurred?: Always (100%) β deterministic for any schema containing such a field.