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

By Cyprian AaronsUpdated 2026-04-21
json-parsing-errorlanggraphpython

A JSON parsing error in LangGraph usually means one of your nodes, tools, or model outputs returned text that LangGraph expected to be valid JSON. It typically shows up when you use structured outputs, tool calling, or a state schema that expects machine-readable data but gets plain text instead.

In practice, this happens when the model returns extra prose, malformed JSON, or Python objects that are not serializable. The fix is usually in the node boundary: make sure every structured output is actually valid JSON and matches the schema LangGraph is expecting.

The Most Common Cause

The #1 cause is returning a string that looks like JSON, but is not valid JSON, from a node or tool. In LangGraph, this often surfaces as something like:

  • json.decoder.JSONDecodeError: Expecting value
  • langchain_core.exceptions.OutputParserException
  • ValueError: Invalid JSON
  • TypeError: Object of type ... is not JSON serializable

Here’s the broken pattern:

BrokenFixed
Returns a Python dict as a stringReturns a real dict/object
Uses single quotes instead of double quotesUses valid JSON-compatible structure
Adds explanatory text around the JSONReturns only structured data
# BROKEN
from langgraph.graph import StateGraph, END
from typing import TypedDict

class State(TypedDict):
    result: dict

def extract_customer_data(state: State):
    # This is NOT valid JSON for downstream parsing
    return {
        "result": "{'name': 'Alice', 'risk': 'low'}"
    }

graph = StateGraph(State)
graph.add_node("extract_customer_data", extract_customer_data)
# FIXED
from langgraph.graph import StateGraph, END
from typing import TypedDict

class State(TypedDict):
    result: dict

def extract_customer_data(state: State):
    # Return a real Python dict; LangGraph can serialize it properly
    return {
        "result": {
            "name": "Alice",
            "risk": "low"
        }
    }

graph = StateGraph(State)
graph.add_node("extract_customer_data", extract_customer_data)

If the failure happens after an LLM call, the same rule applies. Don’t ask for “JSON-ish” output. Force strict structure.

# BROKEN
response = llm.invoke("Return customer summary as JSON.")
print(response.content)  # often includes markdown fences or extra text
# FIXED
from pydantic import BaseModel

class CustomerSummary(BaseModel):
    name: str
    risk: str

structured_llm = llm.with_structured_output(CustomerSummary)
summary = structured_llm.invoke("Extract customer summary from this note.")

Other Possible Causes

1. Model output includes markdown fences

A common failure mode is the model returning:

{
  "name": "Alice"
}

That looks fine to humans, but if your parser expects raw JSON and gets triple backticks, it will fail.

# BROKEN
content = """```json
{"name": "Alice"}
```"""
data = json.loads(content)  # fails
# FIXED
content = '{"name": "Alice"}'
data = json.loads(content)

2. Your state schema does not match what the node returns

LangGraph state updates must match the declared schema. If your graph state expects a dict, but your node returns a string or list, you’ll get serialization/parsing issues.

class State(TypedDict):
    profile: dict

# BROKEN: returns string instead of dict
def node(state: State):
    return {"profile": '{"id": 123}'}

# FIXED: returns actual dict
def node(state: State):
    return {"profile": {"id": 123}}

3. Tool arguments are malformed

If you’re using tool calling with ToolNode, malformed tool args can trigger parsing failures before execution.

# BROKEN tool args example
tool_call = {
    "name": "lookup_policy",
    "args": "{policy_id: 123}"  # invalid JSON syntax
}
# FIXED tool args example
tool_call = {
    "name": "lookup_policy",
    "args": {"policy_id": 123}
}

If you’re manually constructing messages, make sure tool calls use proper message classes like AIMessage with structured tool_calls, not ad-hoc strings.

4. You are storing non-serializable Python objects in state

LangGraph checkpoints and state transitions often require serialization. Objects like database connections, open file handles, datetime objects without conversion, or custom classes can break parsing.

# BROKEN
state_update = {
    "connection": db_connection   # not serializable
}
# FIXED
state_update = {
    "connection_id": "primary-db"
}

For dates and decimals, convert them before storing them in graph state.

state_update = {
    "created_at": created_at.isoformat(),
    "amount": str(amount)
}

How to Debug It

  1. Inspect the exact failing payload

    • Print the raw output from the node right before LangGraph parses it.
    • Look for markdown fences, trailing commentary, single quotes, or Python objects.
  2. Check the stack trace for where parsing fails

    • If you see json.decoder.JSONDecodeError, the problem is almost always malformed JSON input.
    • If you see OutputParserException, it’s usually an LLM response format issue.
    • If you see serialization errors during checkpointing, it’s likely your state contains non-serializable data.
  3. Validate each node boundary

    • Add temporary logging around every node return value.
    • Confirm each node returns only fields defined by your state schema.
    • Make sure tools receive dictionaries, not strings pretending to be dictionaries.
  4. Test with hardcoded valid output

    • Replace the LLM call with a static known-good dict.
    • If the graph works, the issue is upstream in model formatting.
    • If it still fails, your schema or checkpoint configuration is wrong.

Example debug wrapper:

def debug_node(fn):
    def wrapped(state):
        result = fn(state)
        print("NODE RESULT:", result)
        return result
    return wrapped

Prevention

  • Use with_structured_output() or Pydantic models for any LLM output that will be parsed downstream.
  • Keep LangGraph state JSON-safe:
    • strings
    • numbers
    • booleans
    • lists
    • dicts with serializable values only
  • Add unit tests for node outputs and validate them with json.dumps() before wiring them into the graph.

A simple guardrail helps catch this early:

import json

def ensure_json_safe(payload):
    json.dumps(payload)  # raises fast if something is not serializable
    return payload

If you treat every graph edge as a strict contract — input shape in, output shape out — this class of error disappears fast.


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