Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions src/google/adk/models/lite_llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -543,8 +543,11 @@ def _safe_json_serialize(obj) -> str:
try:
# Try direct JSON serialization first
return json.dumps(obj, ensure_ascii=False)
except (TypeError, OverflowError):
return str(obj)
except (TypeError, OverflowError, ValueError, RecursionError):
try:
return str(obj)
except RecursionError:
return '<non-serializable: recursion depth exceeded>'


def _part_has_payload(part: types.Part) -> bool:
Expand Down
2 changes: 1 addition & 1 deletion src/google/adk/telemetry/tracing.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ def _safe_json_serialize(obj) -> str:
return json.dumps(
obj, ensure_ascii=False, default=lambda o: '<not serializable>'
)
except (TypeError, OverflowError):
except (TypeError, OverflowError, ValueError, RecursionError):
return '<not serializable>'


Expand Down
67 changes: 67 additions & 0 deletions tests/unittests/models/test_litellm_safe_serialize.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Tests for _safe_json_serialize in models/lite_llm.py.

Verifies that the function never raises exceptions, even for inputs that
cause json.dumps to raise ValueError (circular references) or
RecursionError (deeply nested structures).

Fixes https://github.com/google/adk-python/issues/5412
"""

import pytest

from google.adk.models.lite_llm import _safe_json_serialize


def test_circular_reference_returns_str_fallback():
"""json.dumps raises ValueError on circular references; should fall back to str()."""

class Node:
def __init__(self):
self.ref = self

obj = Node()
result = _safe_json_serialize(obj)
assert isinstance(result, str)
# Should return str(obj) fallback rather than raising ValueError


def test_deeply_nested_structure_returns_str_fallback():
"""json.dumps raises RecursionError on deeply nested structures."""
obj = current = {}
for _ in range(10000):
current["child"] = {}
current = current["child"]

result = _safe_json_serialize(obj)
assert isinstance(result, str)
# str(obj) itself can raise RecursionError, so expect the safe fallback
assert "recursion" in result.lower() or result # non-empty string


def test_normal_dict_serializes():
"""Normal dicts should serialize as JSON."""
result = _safe_json_serialize({"key": "value", "num": 42})
assert '"key"' in result
assert '"value"' in result


def test_non_serializable_object_falls_back_to_str():
"""Objects without a JSON representation should fall back to str()."""
obj = object()
result = _safe_json_serialize(obj)
assert isinstance(result, str)
assert "object" in result.lower()
65 changes: 65 additions & 0 deletions tests/unittests/telemetry/test_safe_json_serialize.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Tests for _safe_json_serialize in telemetry/tracing.py.

Verifies that the function never raises exceptions, even for inputs that
cause json.dumps to raise ValueError (circular references) or
RecursionError (deeply nested structures).

Fixes https://github.com/google/adk-python/issues/5411
"""

import pytest

from google.adk.telemetry.tracing import _safe_json_serialize


def test_circular_reference_returns_fallback():
"""json.dumps raises ValueError on circular references; should not propagate."""

class Node:
def __init__(self):
self.ref = self

obj = Node()
result = _safe_json_serialize(obj)
assert isinstance(result, str)
# Should return the fallback rather than raising
assert "not serializable" in result.lower() or result # non-empty string


def test_deeply_nested_structure_returns_fallback():
"""json.dumps raises RecursionError on deeply nested structures."""
obj = current = {}
for _ in range(10000):
current["child"] = {}
current = current["child"]

result = _safe_json_serialize(obj)
assert isinstance(result, str)


def test_normal_dict_serializes():
"""Normal dicts should serialize without issue."""
result = _safe_json_serialize({"key": "value", "num": 42})
assert '"key"' in result
assert '"value"' in result


def test_non_serializable_object_uses_default():
"""Objects without a JSON representation use the default callback."""
result = _safe_json_serialize(object())
assert isinstance(result, str)
assert "not serializable" in result.lower()
Loading