SqliteSaver.put raises TypeError on checkpoints whose metadata carries the legacy `writes` key (fixed for Postgres in #6236)
Checked other resources
- This is a bug, not a usage question.
- I added a clear and descriptive title that summarizes this issue.
- I used the GitHub search to find a similar question and didn't find it.
- I am sure that this is a bug in LangGraph rather than my code.
- The bug is not resolved by updating to the latest stable version of LangGraph (or the specific integration package).
- This is not related to the langchain-community package.
- I posted a self-contained, minimal, reproducible example. A maintainer can copy it and run it AS IS.
Related Issues / PRs
- #5769 — the same failure reported against
PostgresSaver - #6236 — the merged fix for it, which added
get_serializable_checkpoint_metadata() - #8701 / #7085 — adjacent but separate; see the last section
Reproduction Steps / Example Code (Python)
from langchain_core.messages import AIMessage
from langgraph.checkpoint.sqlite import SqliteSaver
config = {"configurable": {"thread_id": "1", "checkpoint_ns": ""}}
checkpoint = {
"v": 1,
"id": "1ef4f797-8335-6428-8001-8a1503f9b875",
"ts": "2024-05-04T06:32:42.235444+00:00",
"channel_values": {"messages": []},
"channel_versions": {},
"versions_seen": {},
}
# metadata exactly as an older LangGraph version wrote it
metadata = {
"source": "loop",
"step": 1,
"writes": {"agent": {"messages": [AIMessage(content="hi")]}},
}
with SqliteSaver.from_conn_string(":memory:") as saver:
saver.put(config, checkpoint, metadata, {})
print("stored:", saver.get_tuple(config).metadata)Error Message and Stack Trace (if applicable)
File ".../issue_repro.py", line 21, in <module>
saver.put(config, checkpoint, metadata, {})
File ".../langgraph/checkpoint/sqlite/__init__.py", line 421, in put
serialized_metadata = json.dumps(
...
TypeError: Object of type AIMessage is not JSON serializableAsyncSqliteSaver.aput fails the same way at aio.py:534.
Description
Checkpoints written by older LangGraph versions record node output under metadata["writes"]. Those values are whatever the node returned — for a chat graph, message objects. The SQLite metadata column is JSON, and put() serializes with a plain json.dumps, so writing such a checkpoint back raises and the write is lost.
This is the same bug as #5769, which was reported against PostgresSaver. #6236 fixed it by adding get_serializable_checkpoint_metadata() to langgraph.checkpoint.base — it wraps get_checkpoint_metadata() and pops writes — and pointing the Postgres savers at it. The SQLite savers were not updated and still call get_checkpoint_metadata():
| call site | helper |
|---|---|
checkpoint-postgres/.../postgres/__init__.py:342 |
get_serializable_checkpoint_metadata |
checkpoint-postgres/.../postgres/aio.py:302 |
get_serializable_checkpoint_metadata |
checkpoint-postgres/.../postgres/shallow.py:447, :785 |
get_serializable_checkpoint_metadata |
checkpoint-sqlite/.../sqlite/__init__.py:422 |
get_checkpoint_metadata ← raises |
checkpoint-sqlite/.../sqlite/aio.py:535 |
get_checkpoint_metadata ← raises |
So the two backends currently disagree about whether the same put() succeeds. On main (230927fb3) the snippet above raises on SQLite; the identical metadata goes through Postgres, which drops the key and stores the rest.
Two things worth flagging before anyone fixes this:
It is a behaviour change for a JSON-serializable writes. SQLite persists {"writes": {"foo": "bar"}} today and would stop. That is the call #6236 already made for Postgres, so the fix brings the backends into agreement rather than introducing a new asymmetry — but it is a visible change, not purely a crash fix.
The SQLite test fixtures currently carry writes, and two of them need it. test_search_where and test_metadata_predicate feed metadata_1/metadata_2 to the SQL predicate builder as filter inputs, and they are the only tests covering a nested mapping being JSON-encoded into a bound parameter (writes: {"foo": "bar"} → '{"foo":"bar"}'). Deleting writes from the shared fixtures the way #6236 did for Postgres silently removes that coverage. Those two tests need their own filter dicts.
InMemorySaver (checkpoint/.../memory/__init__.py:454) also calls get_checkpoint_metadata(), but it serializes with msgpack, so it stores the key instead of raising. Different severity, different package — I have left it out of scope here.
I have the SQLite fix ready on fix/sqlite-metadata-writes: both savers moved to the shared helper, the two predicate tests decoupled so the nested-mapping coverage survives, and a sync and async regression test for the snippet above. 119 passed in libs/checkpoint-sqlite including the conformance delta suite; reverting either saver line individually fails exactly one of the two new tests. ruff check, ruff format --check and ty check are clean. I could not run tests/test_ttl.py (needs pytest-retry) or anything Postgres (no Docker here), and this change touches neither.
Could I be assigned this? Happy to open the PR as soon as I am.
Not overlapping with: #8701 (Whxuan0701) asks for conformance coverage of nested JSON-compatible metadata across savers, and states "do not change serialization formats" as a non-goal — the opposite of this change; I checked that my branch leaves nested mappings, empty maps and empty lists round-tripping unchanged, so nothing there is silently satisfied. #7085 (Charank18) adds unit tests for the checkpoint.base helpers in libs/checkpoint/tests/test_base.py; my change is in libs/checkpoint-sqlite and touches no file it does. I also checked the nine open PRs against the two SQLite saver files (#8557, #8544, #8242, #7207, #7204, #7180, #7105, #6939, #6871) — all delta replay, ordering or pagination, none touches metadata serialization.
System Info
langgraph 1.2.11
langgraph-checkpoint 4.2.0
langgraph-checkpoint-sqlite 3.1.1
langchain-core 1.6.1
Python 3.13.12
OS Windows 11Source: langchain-ai/langgraph