From f6f96b14382e639e3ddbe9a8a1f32b05570920da Mon Sep 17 00:00:00 2001
From: OpenClaw <openclaw@proton.me>
Date: Wed, 4 Feb 2026 02:38:50 +0100
Subject: [PATCH] fix: handle nested enum serialization in checkpoint serde

Uses __qualname__ instead of __name__ for enum classes, which preserves
the full qualified path (e.g., "DatasetArtifact.PhaseEnum" instead of
just "PhaseEnum").

During deserialization, walks the qualified name path to resolve nested
classes correctly.

Includes regression test with a Pydantic model containing a nested enum.

Fixes #6718
---
 .../langgraph/checkpoint/serde/jsonplus.py    | 18 +++++++--
 libs/checkpoint/tests/test_jsonplus.py        | 39 +++++++++++++++++++
 2 files changed, 53 insertions(+), 4 deletions(-)

diff --git a/libs/checkpoint/langgraph/checkpoint/serde/jsonplus.py b/libs/checkpoint/langgraph/checkpoint/serde/jsonplus.py
index c4b5503..e835bc7 100644
--- a/libs/checkpoint/langgraph/checkpoint/serde/jsonplus.py
+++ b/libs/checkpoint/langgraph/checkpoint/serde/jsonplus.py
@@ -390,7 +390,7 @@ def _msgpack_default(obj: Any) -> str | ormsgpack.Ext:
         return ormsgpack.Ext(
             EXT_CONSTRUCTOR_SINGLE_ARG,
             _msgpack_enc(
-                (obj.__class__.__module__, obj.__class__.__name__, obj.value),
+                (obj.__class__.__module__, obj.__class__.__qualname__, obj.value),
             ),
         )
     elif isinstance(obj, SendProtocol):
@@ -454,10 +454,20 @@ def _msgpack_ext_hook(code: int, data: bytes) -> Any:
             tup = ormsgpack.unpackb(
                 data, ext_hook=_msgpack_ext_hook, option=ormsgpack.OPT_NON_STR_KEYS
             )
-            # module, name, arg
-            return getattr(importlib.import_module(tup[0]), tup[1])(tup[2])
+            # module, name (may be qualified like "Parent.Nested"), arg
+            mod = importlib.import_module(tup[0])
+            # Handle qualified names (e.g., "DatasetArtifact.PhaseEnum")
+            cls = mod
+            for attr in tup[1].split("."):
+                cls = getattr(cls, attr)
+            return cls(tup[2])
         except Exception:
-            return
+            # Fall back to returning the raw value (e.g., enum string)
+            # so Pydantic can coerce it back to the correct type
+            try:
+                return tup[2]
+            except NameError:
+                return
     elif code == EXT_CONSTRUCTOR_POS_ARGS:
         try:
             tup = ormsgpack.unpackb(
diff --git a/libs/checkpoint/tests/test_jsonplus.py b/libs/checkpoint/tests/test_jsonplus.py
index c2ff5f2..78da9ce 100644
--- a/libs/checkpoint/tests/test_jsonplus.py
+++ b/libs/checkpoint/tests/test_jsonplus.py
@@ -77,6 +77,17 @@ class MyEnum(Enum):
     BAR = "bar"
 
 
+class OuterModel(BaseModel):
+    """Pydantic model with a nested enum defined inside the class."""
+
+    class NestedEnum(Enum):
+        ALPHA = "alpha"
+        BETA = "beta"
+
+    value: NestedEnum
+    name: str = "test"
+
+
 @dataclasses_json.dataclass_json
 @dataclasses.dataclass
 class Person:
@@ -167,6 +178,34 @@ def test_serde_jsonplus() -> None:
     assert serde.loads_typed(serde.dumps_typed(surrogates)) == surrogates
 
 
+def test_serde_nested_enum() -> None:
+    """Test that nested enums (enums defined inside a class) serialize correctly.
+
+    Regression test for https://github.com/langchain-ai/langgraph/issues/6718
+    """
+    serde = JsonPlusSerializer()
+
+    # Create a model with a nested enum
+    model = OuterModel(value=OuterModel.NestedEnum.ALPHA, name="test")
+
+    # Serialize and deserialize
+    dumped = serde.dumps_typed(model)
+    assert dumped[0] == "msgpack"
+
+    result = serde.loads_typed(dumped)
+
+    # Verify the result is a proper OuterModel with the enum intact
+    assert isinstance(result, OuterModel)
+    assert result.value == OuterModel.NestedEnum.ALPHA
+    assert result.name == "test"
+
+    # Also test the enum value directly
+    enum_val = OuterModel.NestedEnum.BETA
+    dumped_enum = serde.dumps_typed(enum_val)
+    result_enum = serde.loads_typed(dumped_enum)
+    assert result_enum == OuterModel.NestedEnum.BETA
+
+
 def test_serde_jsonplus_json_mode() -> None:
     uid = uuid.UUID(int=1)
     deque_instance = deque([1, 2, 3])
-- 
2.43.0

