From 1f922a80f539435eb6328d1adbdfd5137f281495 Mon Sep 17 00:00:00 2001 From: Rodion Kazennov Date: Thu, 8 Oct 2026 08:42:35 +0000 Subject: [PATCH 1/2] Python: make_json_safe serializes plain Enum members by value A plain Enum member is not a scalar and has no to_dict() or model_dump(), so make_json_safe() reached the vars() fallback; the member's __objclass__ points back at the enum class, and walking it recursed until RecursionError. Return make_json_safe(member.value) instead, right after the scalar check, so str and int mixin enums keep their current output. Fixes #9194 --- .../core/agent_framework/_serialization.py | 11 ++++-- .../core/tests/core/test_observability.py | 37 +++++++++++++++++++ 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/python/packages/core/agent_framework/_serialization.py b/python/packages/core/agent_framework/_serialization.py index 3751688f91c..5e53017ff8a 100644 --- a/python/packages/core/agent_framework/_serialization.py +++ b/python/packages/core/agent_framework/_serialization.py @@ -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 @@ -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. @@ -746,6 +747,10 @@ def make_json_safe(obj: Any) -> Any: """ if isinstance(obj, _JSON_SCALAR_TYPES): return obj + if isinstance(obj, Enum): + # 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, (datetime, date)): return obj.isoformat() if isinstance(obj, (bytes, bytearray)): diff --git a/python/packages/core/tests/core/test_observability.py b/python/packages/core/tests/core/test_observability.py index fb4a5b1d38c..c2c69f28911 100644 --- a/python/packages/core/tests/core/test_observability.py +++ b/python/packages/core/tests/core/test_observability.py @@ -2808,6 +2808,43 @@ 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_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 From 55ad9f63ae593adc2e0b399a4c1fde684e5f5758 Mon Sep 17 00:00:00 2001 From: Rodion Kazennov Date: Sat, 10 Oct 2026 02:26:26 +0000 Subject: [PATCH 2/2] Python: keep an enum's own serializer hook in make_json_safe Move the Enum branch after the model_dump/to_dict/dict hooks, so an enum that defines one keeps its serializer and only a plain member falls back to its value. Adds a regression test for the three hooks. --- .../core/agent_framework/_serialization.py | 9 +++--- .../core/tests/core/test_observability.py | 29 +++++++++++++++++++ 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/python/packages/core/agent_framework/_serialization.py b/python/packages/core/agent_framework/_serialization.py index 5e53017ff8a..257ac095dc8 100644 --- a/python/packages/core/agent_framework/_serialization.py +++ b/python/packages/core/agent_framework/_serialization.py @@ -747,10 +747,6 @@ def make_json_safe(obj: Any) -> Any: """ if isinstance(obj, _JSON_SCALAR_TYPES): return obj - if isinstance(obj, Enum): - # 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, (datetime, date)): return obj.isoformat() if isinstance(obj, (bytes, bytearray)): @@ -776,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)): diff --git a/python/packages/core/tests/core/test_observability.py b/python/packages/core/tests/core/test_observability.py index c2c69f28911..e183a746e82 100644 --- a/python/packages/core/tests/core/test_observability.py +++ b/python/packages/core/tests/core/test_observability.py @@ -2825,6 +2825,35 @@ class Color(Enum): 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