Skip to content

output_schema validation dumps in pydantic python mode, so the node result is not JSON-serializableΒ #6747

Description

@itayro1

πŸ”΄ 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()
    
  •  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.

Metadata

Metadata

Labels

core[Component] This issue is related to the core interface and implementationrequest clarification[Status] The maintainer need clarification or more information from the author

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions