diff --git a/libs/checkpoint/langgraph/checkpoint/serde/jsonplus.py b/libs/checkpoint/langgraph/checkpoint/serde/jsonplus.py index 4ef4a40b2..e8c273c3b 100644 --- a/libs/checkpoint/langgraph/checkpoint/serde/jsonplus.py +++ b/libs/checkpoint/langgraph/checkpoint/serde/jsonplus.py @@ -494,6 +494,13 @@ def _msgpack_default(obj: Any) -> str | ormsgpack.Ext: elif isinstance(obj, BaseException): return repr(obj) + elif type(obj).__str__ is not object.__str__: + return ormsgpack.Ext( + EXT_CONSTRUCTOR_SINGLE_ARG, + _msgpack_enc( + (obj.__class__.__module__, obj.__class__.__name__, str(obj)), + ), + ) else: raise TypeError(f"Object of type {obj.__class__.__name__} is not serializable") diff --git a/libs/checkpoint/tests/test_jsonplus.py b/libs/checkpoint/tests/test_jsonplus.py index 9242c59bf..46a17c652 100644 --- a/libs/checkpoint/tests/test_jsonplus.py +++ b/libs/checkpoint/tests/test_jsonplus.py @@ -93,6 +93,19 @@ class MyEnum(Enum): BAR = "bar" +class StrReconstructable: + """Mimics types like bson.ObjectId that round-trip via str().""" + + def __init__(self, value: str = "default") -> None: + self.value = value + + def __str__(self) -> str: + return self.value + + def __eq__(self, other: object) -> bool: + return isinstance(other, StrReconstructable) and self.value == other.value + + @dataclasses_json.dataclass_json @dataclasses.dataclass class Person: @@ -580,6 +593,32 @@ def test_msgpack_safe_types_no_warning(caplog: pytest.LogCaptureFixture) -> None assert result is not None +def test_serde_str_reconstructable_roundtrip() -> None: + """Objects with custom __str__ serialize via the str-fallback and round-trip.""" + serde = JsonPlusSerializer( + allowed_msgpack_modules=[ + (StrReconstructable.__module__, StrReconstructable.__name__) + ] + ) + obj = StrReconstructable("abc123") + dumped = serde.dumps_typed(obj) + assert dumped[0] == "msgpack" + result = serde.loads_typed(dumped) + assert isinstance(result, StrReconstructable) + assert result == obj + + +def test_serde_str_fallback_not_triggered_for_plain_object() -> None: + """Objects without custom __str__ still raise TypeError.""" + serde = JsonPlusSerializer() + + class PlainObj: + pass + + with pytest.raises(ormsgpack.MsgpackEncodeError): + serde.dumps_typed(PlainObj()) + + def test_msgpack_pydantic_warns_by_default(caplog: pytest.LogCaptureFixture) -> None: """Pydantic models not in allowlist should log warning but still deserialize.""" current = _lg_msgpack.STRICT_MSGPACK_ENABLED