How to Fix 'JSON parsing error when scaling' in LangGraph (Python)
Opening
If you’re seeing JSON parsing error when scaling in LangGraph, it usually means one of your graph state values cannot be serialized cleanly when the runtime tries to persist, checkpoint, or pass state between nodes. In practice, this shows up when you scale from a local happy-path run to a threaded, checkpointed, or distributed setup.
The root problem is almost always the same: something in your state is not valid JSON, or you’re returning a shape that LangGraph’s serializer cannot round-trip.
The Most Common Cause
The #1 cause is putting non-JSON-serializable objects into graph state. That includes Python objects like datetime, Decimal, set, custom classes, open file handles, DB connections, and sometimes raw Pydantic models depending on how you return them.
In LangGraph, this usually blows up during checkpointing with errors like:
- •
TypeError: Object of type datetime is not JSON serializable - •
json.decoder.JSONDecodeError: Expecting value - •
langgraph.checkpoint.base.CheckpointError - •
langgraph.errors.InvalidUpdateErrorwhen the node returns an invalid state shape
Broken vs fixed pattern
| Broken pattern | Fixed pattern |
|---|---|
| Returns raw Python objects in state | Converts everything to JSON-safe primitives |
| Stores complex objects directly | Stores IDs / strings / dicts instead |
| Lets node return arbitrary objects | Returns a plain dict matching the schema |
# BROKEN
from datetime import datetime
from langgraph.graph import StateGraph, END
from typing import TypedDict
class State(TypedDict):
timestamp: str
result: str
def node(state: State):
return {
"timestamp": datetime.utcnow(), # not JSON serializable
"result": {"ok": True}, # wrong type for result
}
builder = StateGraph(State)
builder.add_node("node", node)
builder.set_entry_point("node")
builder.add_edge("node", END)
graph = builder.compile()
# FIXED
from datetime import datetime
from langgraph.graph import StateGraph, END
from typing import TypedDict
class State(TypedDict):
timestamp: str
result: dict
def node(state: State):
return {
"timestamp": datetime.utcnow().isoformat(), # JSON-safe string
"result": {"ok": True},
}
builder = StateGraph(State)
builder.add_node("node", node)
builder.set_entry_point("node")
builder.add_edge("node", END)
graph = builder.compile()
If you are using checkpointing, this matters even more because LangGraph persists each step. A value that looks fine in-memory can still fail as soon as the checkpointer tries to serialize it.
Other Possible Causes
1. Returning the wrong update shape from a node
LangGraph nodes must return a partial state update that matches the graph schema. If you return a list, tuple, or nested object where the schema expects flat keys, you can trigger serialization or validation failures.
# BROKEN
def node(state):
return ["a", "b"]
# FIXED
def node(state):
return {"messages": ["a", "b"]}
This often surfaces as:
- •
langgraph.errors.InvalidUpdateError: Expected dict - •
TypeErrorduring downstream serialization
2. Mixing message objects and raw dicts incorrectly
If you use chat state with MessagesState, don’t shove custom objects into the messages list. Keep message entries compatible with LangChain/LangGraph message types.
# BROKEN
state = {
"messages": [
{"role": "user", "content": "hi"},
object(), # breaks serialization
]
}
# FIXED
from langchain_core.messages import HumanMessage, AIMessage
state = {
"messages": [
HumanMessage(content="hi"),
AIMessage(content="hello"),
]
}
If you need custom metadata, put it in separate scalar fields in state.
3. Using a checkpointer with unserializable config values
Sometimes the graph state is fine, but your config payload is not. This happens when passing objects through configurable that the checkpointer tries to persist.
# BROKEN
config = {
"configurable": {
"thread_id": "abc",
"db_conn": some_db_connection,
}
}
# FIXED
config = {
"configurable": {
"thread_id": "abc",
"db_name": "primary",
}
}
Keep config values primitive: strings, numbers, booleans, lists, dicts of primitives.
4. Custom reducers returning unsupported types
If you use annotated reducers for list accumulation or merging state and the reducer returns a non-serializable object, scaling will fail later in execution.
# BROKEN reducer output example
def merge_messages(left, right):
return set(left + right) # sets are not JSON serializable
# FIXED
def merge_messages(left, right):
return left + right # list stays serializable
This is easy to miss because the reducer may work locally until persistence kicks in.
How to Debug It
- •
Print the exact state returned by each node
- •Add logging right before every
return. - •Look for
datetime,set, custom classes, DB sessions, and nested objects. - •If needed:
import json print(json.dumps(update))
- •Add logging right before every
- •
Run without checkpointing first
- •If the error disappears when you remove the checkpointer, the issue is almost certainly serialization-related.
- •Then re-enable checkpointing and isolate which field breaks persistence.
- •
Validate each node output against plain JSON
- •Serialize each update with:
json.dumps(node_output) - •If that fails, LangGraph will fail too.
- •Fix by converting to strings, ints, floats, bools, lists, and dicts only.
- •Serialize each update with:
- •
Check the stack trace for where it fails
- •If it fails inside
langgraph.checkpoint.*, inspect persisted state. - •If it fails inside
langgraph.errors.InvalidUpdateError, your node probably returned the wrong shape. - •If it fails after an LLM/tool call, inspect tool outputs and message formatting.
- •If it fails inside
Prevention
- •
Keep graph state boring:
- •Use only JSON-safe primitives in state.
- •Store object IDs or serialized representations instead of live Python objects.
- •
Define strict schemas:
- •Use
TypedDictor Pydantic models for graph state. - •Make sure every node returns exactly what that schema expects.
- •Use
- •
Test serialization early:
- •Add a unit test that runs
json.dumps()on every node output. - •Catch bad types before they hit production checkpoints or scaled workers.
- •Add a unit test that runs
Keep learning
- •The complete AI Agents Roadmap — my full 8-step breakdown
- •Free: The AI Agent Starter Kit — PDF checklist + starter code
- •Work with me — I build AI for banks and insurance companies
By Cyprian Aarons, AI Consultant at Topiax.
Want the complete 8-step roadmap?
Grab the free AI Agent Starter Kit — architecture templates, compliance checklists, and a 7-email deep-dive course.
Get the Starter Kit