Skip to content
Merged
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
12 changes: 9 additions & 3 deletions python/packages/core/agent_framework/_serialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from collections.abc import Mapping, MutableMapping
from dataclasses import asdict, is_dataclass
from datetime import date, datetime, time
from enum import Enum
from functools import lru_cache
from typing import Any, ClassVar, Final, Protocol, TypeGuard, TypeVar, cast, runtime_checkable

Expand Down Expand Up @@ -734,9 +735,9 @@ def make_json_safe(obj: Any) -> Any:
"""Recursively convert an object to a JSON-serializable form.
Handles dataclasses, Pydantic models, objects with ``to_dict``/``dict``/``__dict__``,
datetimes, bytes (base64), lists, dicts, and primitives. Falls back to ``str()`` for
any remaining non-serializable value so that ``json.dumps`` never raises a
``TypeError``.
enum members (as their values), datetimes, bytes (base64), lists, dicts, and primitives.
Falls back to ``str()`` for any remaining non-serializable value so that ``json.dumps``
never raises a ``TypeError``.
Args:
obj: Object to make JSON safe.
Expand Down Expand Up @@ -771,6 +772,11 @@ def make_json_safe(obj: Any) -> Any:
return make_json_safe(obj.dict()) # type: ignore[no-any-return]
except TypeError:
pass
if isinstance(obj, Enum):
# After the serializer hooks, so an enum that defines one keeps it. A plain Enum member
# is not a scalar, and its ``__dict__`` points back at the enum class, so the generic
# fallback below would recurse until RecursionError.
return make_json_safe(obj.value)
if isinstance(obj, dict):
return {str(key): make_json_safe(value) for key, value in obj.items()} # type: ignore[misc]
if isinstance(obj, (list, tuple)):
Expand Down
66 changes: 66 additions & 0 deletions python/packages/core/tests/core/test_observability.py
Original file line number Diff line number Diff line change
Expand Up @@ -2808,6 +2808,72 @@ def model_dump(self, required: str) -> dict[str, str]:
assert result == {"value": "fallback"}


def test_make_json_safe_plain_enum_uses_its_value():
"""Test make_json_safe serializes plain Enum members as their values instead of walking the enum class."""
import json
from enum import Enum

from agent_framework._serialization import make_json_safe

class Color(Enum):
RED = 1
LABEL = "label"

result = make_json_safe({"color": Color.RED, "label": Color.LABEL, "nested": [Color.RED]})

assert result == {"color": 1, "label": "label", "nested": [1]}
assert json.loads(json.dumps(result)) == result


def test_make_json_safe_enum_with_serializer_hook_keeps_it():
"""Test make_json_safe uses an enum's own to_dict/model_dump/dict hook instead of its raw value."""
from enum import Enum

from agent_framework._serialization import make_json_safe

class Shape(Enum):
SQUARE = 4

def to_dict(self):
return {"name": self.name, "sides": self.value}

class Mode(Enum):
FAST = "fast"

def model_dump(self):
return {"mode": self.value, "label": "Fast"}

class Unit(Enum):
METRE = "m"

def dict(self):
return {"symbol": self.value}

assert make_json_safe({"shape": Shape.SQUARE}) == {"shape": {"name": "SQUARE", "sides": 4}}
assert make_json_safe(Mode.FAST) == {"mode": "fast", "label": "Fast"}
assert make_json_safe([Unit.METRE]) == [{"symbol": "m"}]


def test_make_json_safe_mixin_enums_are_unchanged():
"""Test make_json_safe keeps str and int mixin enums as the scalars they already are."""
import json
from enum import Enum, IntEnum

from agent_framework._serialization import make_json_safe

class Level(str, Enum):
HIGH = "high"

class Priority(IntEnum):
LOW = 1

result = make_json_safe({"level": Level.HIGH, "priority": Priority.LOW})

assert result["level"] is Level.HIGH
assert result["priority"] is Priority.LOW
assert json.dumps(result) == '{"level": "high", "priority": 1}'


def test_make_json_safe_dict_with_non_string_keys():
"""Test make_json_safe converts non-primitive dict keys to strings."""
import json
Expand Down
Loading