-
Notifications
You must be signed in to change notification settings - Fork 4.3k
Description
Checked other resources
- This is a bug, not a usage question. For questions, please use the LangChain Forum (https://forum.langchain.com/).
- I added a clear and detailed title that summarizes the issue.
- I read what a minimal reproducible example is (https://stackoverflow.com/help/minimal-reproducible-example).
- I included a self-contained, minimal example that demonstrates the issue INCLUDING all the relevant imports. The code run AS IS to reproduce the issue.
Example Code
from enum import StrEnum
from typing import TypedDict
from pydantic import BaseModel
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import StateGraph, END, START
from langgraph.types import interrupt, Command
# Pydantic model with nested enum (enum defined inside class)
class DatasetArtifact(BaseModel):
class PhaseEnum(StrEnum):
QUERY = "query_ready"
READY = "ready"
phase: PhaseEnum
item_id: str | None = None
# Graph state
class GraphState(TypedDict):
artifact: DatasetArtifact
# Track execution step
_execution_count = 0
# Node that interrupts (checkpoints state)
def checkpoint_node(state: GraphState):
global _execution_count
_execution_count += 1
artifact = state['artifact']
if _execution_count == 1:
# First execution: before checkpoint, enum is correct
print(f"[BEFORE CHECKPOINT] Artifact type: {type(artifact)}")
print(f"[BEFORE CHECKPOINT] Phase type: {type(artifact.phase)}")
print(f"[BEFORE CHECKPOINT] Phase value: {artifact.phase}")
assert isinstance(artifact, DatasetArtifact)
assert artifact.phase == DatasetArtifact.PhaseEnum.QUERY
else:
# Second execution: after resume, state loaded from checkpoint (BUG HERE!)
print(f"Artifact type: {type(artifact)}")
print(f"Phase type: {type(artifact.phase)}")
print(f"Phase value: {artifact.phase}")
# This will fail because phase is None
assert artifact.phase == DatasetArtifact.PhaseEnum.QUERY, "Phase should be PhaseEnum.QUERY"
return interrupt("Need approval")
# Node that runs after resume
def after_resume_node(state: GraphState):
return state
# Build graph
graph = StateGraph(GraphState)
graph.add_node("checkpoint", checkpoint_node)
graph.add_node("resume", after_resume_node)
graph.add_edge(START, "checkpoint")
graph.add_edge("checkpoint", "resume")
graph.add_edge("resume", END)
# Compile with checkpointer
checkpointer = InMemorySaver()
compiled = graph.compile(checkpointer=checkpointer)
# Initial run - creates checkpoint after interrupt
print("=" * 60)
print("STEP 1: Initial run (creates checkpoint)")
print("=" * 60)
config = {"configurable": {"thread_id": "test-1"}}
initial_state = {"artifact": DatasetArtifact(phase=DatasetArtifact.PhaseEnum.QUERY, item_id="123")}
result1 = compiled.invoke(initial_state, config=config)
print("\nCheckpoint created after interrupt")
# Resume from checkpoint - state is loaded from checkpoint
print("\n" + "=" * 60)
print("STEP 2: Resume from checkpoint (loads state)")
print("=" * 60)
try:
result2 = compiled.invoke(Command(resume={}), config=config)
except AssertionError as e:
# Expected: assertion fails because phase is None
print(f"\n BUG: {e}")
print("\nFull stack trace:")
import traceback
traceback.print_exc()
# Expected: result2['artifact'].phase is PhaseEnum.QUERY
# Actual: result2['artifact'].phase is NoneError Message and Stack Trace (if applicable)
============================================================
STEP 1: Initial run (creates checkpoint)
============================================================
[BEFORE CHECKPOINT] Artifact type: <class '__main__.DatasetArtifact'>
[BEFORE CHECKPOINT] Phase type: <enum 'PhaseEnum'>
[BEFORE CHECKPOINT] Phase value: query_ready
[BEFORE CHECKPOINT] Is DatasetArtifact: True
Checkpoint created after interrupt
============================================================
STEP 2: Resume from checkpoint (loads state)
============================================================
Artifact type: <class '__main__.DatasetArtifact'>
Phase type: <class 'NoneType'>
Phase value: None
Is DatasetArtifact: True
BUG: Phase should be PhaseEnum.QUERY
Traceback (most recent call last):
File "test_nested_enum_bug.py", line 88, in <module>
result2 = compiled.invoke(Command(resume={}), config=config)
File ".../langgraph/pregel/main.py", line 3026, in invoke
for chunk in self.stream(
~~~~~~~~~~~^
input,
^^^^^^
...<10 lines>...
**kwargs,
^^^^^^^^^
):
^
File ".../langgraph/pregel/main.py", line 2647, in stream
for _ in runner.tick(
~~~~~~~~~~~^
[t for t in loop.tasks.values() if not t.writes],
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
...<2 lines>...
schedule_task=loop.accept_push,
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
):
^
File ".../langgraph/pregel/_runner.py", line 162, in tick
run_with_retry(
~~~~~~~~~~~~~~^
t,
^^
...<10 lines>...
},
^^
)
^
File ".../langgraph/pregel/_retry.py", line 42, in run_with_retry
return task.proc.invoke(task.input, config)
~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^
File ".../langgraph/_internal/_runnable.py", line 657, in invoke
input = context.run(step.invoke, input, config, **kwargs)
File ".../langgraph/_internal/_runnable.py", line 401, in invoke
ret = self.func(*args, **kwargs)
File "test_nested_enum_bug.py", line 54, in checkpoint_node
assert artifact.phase == DatasetArtifact.PhaseEnum.QUERY, "Phase should be PhaseEnum.QUERY"
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError: Phase should be PhaseEnum.QUERY
During task with name 'checkpoint' and id 'f703daeb-106d-cf27-274c-84d7f5c1f0a3'Description
What I'm doing:
Using LangGraph checkpointing with Pydantic models that contain nested enum fields (enums defined inside a class, like DatasetArtifact.PhaseEnum).
What I expect:
After checkpointing and resuming, the enum field should remain as the enum instance (e.g., PhaseEnum.QUERY), and the Pydantic model should remain as a model instance.
What actually happens:
- The enum field becomes
Noneafter checkpoint deserialization - Pydantic validation fails because
phase: Nonedoesn't matchphase: PhaseEnum - The Pydantic model falls back to a dict instead of a model instance
- Code that expects a model instance gets
AttributeError: 'dict' object has no attribute '...'
Root cause:
In langgraph/checkpoint/serde/jsonplus.py, the _msgpack_ext_hook() function (line ~380) handles EXT_CONSTRUCTOR_SINGLE_ARG (used for enums) by trying to import the enum class directly from the module:
# Current code
return getattr(importlib.import_module(tup[0]), tup[1])(tup[2])
# Tries: getattr(module, "PhaseEnum")("query_ready")For nested enums (e.g., DatasetArtifact.PhaseEnum), this fails because I bieleve:
- The enum is not at module level:
module.PhaseEnumdoesn't exist - It's nested:
module.DatasetArtifact.PhaseEnumexists getattr(module, "PhaseEnum")raisesAttributeError- Exception handler returns
Noneinstead of the enum value
Impact:
- Breaks Pydantic validation for models with nested enum fields
- Causes models to become dicts instead of model instances
- Leads to
AttributeErrorwhen accessing model methods/attributes
Workaround:
Currently using a Pydantic @model_validator(mode="before") to fix None enums and re-instantiate models, but this shouldn't be necessary.
Additional Context
Related issues:
- Issue Checkpointer silently converts
StrEnumfields intostrwhen serializing/deserializing #6598 mentions enum serialization but focuses on StrEnum conversion, not nested enums - This is specifically about nested enums (enums defined inside classes) becoming None
Suggested fix:
When enum deserialization fails in _msgpack_ext_hook(), return the enum value (string) instead of None. This allows Pydantic to validate the value and reconstruct the enum correctly.
Location: langgraph/checkpoint/serde/jsonplus.py, _msgpack_ext_hook() function, EXT_CONSTRUCTOR_SINGLE_ARG case (line ~380)
System Info
python -m langchain_core.sys_info
System Information
OS: Linux
OS Version: #91-Ubuntu SMP PREEMPT_DYNAMIC Tue Nov 18 14:14:30 UTC 2025
Python Version: 3.13.5 | packaged by Anaconda, Inc. | (main, Jun 12 2025, 16:09:02) [GCC 11.2.0]
Package Information
langchain_core: 1.1.3
langchain: 1.1.3
langsmith: 0.4.58
langchain_classic: 1.0.0
langchain_openai: 1.1.1
langchain_text_splitters: 1.0.0
langgraph_sdk: 0.2.15
Optional packages not installed
langserve
Other Dependencies
async-timeout: 5.0.1
httpx: 0.28.1
jsonpatch: 1.33
langgraph: 1.0.4
openai: 2.9.0
opentelemetry-api: 1.39.0
opentelemetry-exporter-otlp-proto-http: 1.39.0
opentelemetry-sdk: 1.39.0
orjson: 3.11.5
packaging: 25.0
pydantic: 2.12.5
pytest: 9.0.2
pyyaml: 6.0.3
requests: 2.32.5
requests-toolbelt: 1.0.0
rich: 14.2.0
sqlalchemy: 2.0.45
tenacity: 9.1.2
tiktoken: 0.12.0
typing-extensions: 4.15.0
uuid-utils: 0.12.0
zstandard: 0.25.0