How to Fix 'JSON parsing error when scaling' in LangGraph (Python)

By Cyprian AaronsUpdated 2026-04-21
json-parsing-error-when-scalinglanggraphpython

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.InvalidUpdateError when the node returns an invalid state shape

Broken vs fixed pattern

Broken patternFixed pattern
Returns raw Python objects in stateConverts everything to JSON-safe primitives
Stores complex objects directlyStores IDs / strings / dicts instead
Lets node return arbitrary objectsReturns 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
  • TypeError during 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

  1. 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))
      
  2. 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.
  3. 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.
  4. 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.

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 TypedDict or Pydantic models for graph state.
    • Make sure every node returns exactly what that schema expects.
  • 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.

Keep learning

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

Related Guides