TypeError: Got unknown type LangGraph Message Conversion (2026)

If you use LangGraph with LangChain and see TypeError: Got unknown type X during message conversion, the root cause is passing a raw dict or a message type that LangChain’s converter cannot recognize.

This shows up when you mix custom message shapes with the built-in chat model wrappers. Here is the exact fix pattern that works on LangChain 0.3+ and LangGraph 0.2+ in 2026.

TypeError Got unknown type LangGraph Message Conversion (2026)

Why the TypeError happens

LangChain’s message converter expects one of the typed classes: HumanMessage, AIMessage, SystemMessage, ToolMessage, or FunctionMessage. When you pass a raw dict like {"role": "user", "content": "hi"} or a custom subclass without the right type annotation, the converter cannot map it to a provider format and raises TypeError. LangGraph inherits this restriction because its state uses LangChain messages internally.

The fix: always use typed message classes

from langchain_core.messages import (
    HumanMessage,
    AIMessage,
    SystemMessage,
    ToolMessage,
)
from langchain_anthropic import ChatAnthropic
from langgraph.graph import StateGraph, END
from typing import TypedDict, Annotated
from langgraph.graph.message import add_messages

class State(TypedDict):
    messages: Annotated[list, add_messages]

llm = ChatAnthropic(model="claude-opus-4-8")

def chat_node(state: State) -> State:
    # WRONG: dict messages crash the converter
    # bad = [{"role": "user", "content": "hello"}]

    # CORRECT: typed message classes
    good = [
        SystemMessage(content="You are a helpful assistant."),
        HumanMessage(content="Explain LangGraph in 2 sentences."),
    ]
    reply = llm.invoke(good)
    return {"messages": [reply]}

graph = StateGraph(State)
graph.add_node("chat", chat_node)
graph.set_entry_point("chat")
graph.add_edge("chat", END)
app = graph.compile()

result = app.invoke({"messages": []})
print(result["messages"][-1].content)

Converting from dicts when you must

If your app receives dicts from an API or frontend, convert to typed messages before passing to LangChain or LangGraph.

from langchain_core.messages import HumanMessage, AIMessage, SystemMessage

ROLE_TO_CLASS = {
    "system": SystemMessage,
    "user": HumanMessage,
    "assistant": AIMessage,
}

def dict_to_message(d: dict):
    cls = ROLE_TO_CLASS.get(d["role"])
    if not cls:
        raise ValueError(f"Unknown role: {d['role']}")
    return cls(content=d["content"])

# From frontend or REST API
raw = [
    {"role": "system", "content": "Answer briefly."},
    {"role": "user", "content": "What is LangGraph?"},
]
typed = [dict_to_message(d) for d in raw]

# Now safe to pass to any LangChain chat model
reply = llm.invoke(typed)

Debugging checklist

  • Print the type of every message before invoking the LLM: print([type(m).__name__ for m in messages]). Any dict or str in the list is the culprit.
  • Verify your langchain-core version. TypeError message shape changed in 0.3+. If your traceback text does not match modern docs, upgrade first.
  • Check any custom retriever or state reducer that appends messages. A common bug is appending a plain string reply from a tool instead of wrapping it in ToolMessage.
  • If you use add_messages reducer in LangGraph state, confirm every append passes typed messages. The reducer does not silently coerce dicts.

Real-world example: tool-calling agent that avoids the TypeError

from langchain_anthropic import ChatAnthropic
from langchain_core.messages import HumanMessage, AIMessage, ToolMessage
from langchain_core.tools import tool
from langgraph.prebuilt import create_react_agent

@tool
def get_weather(city: str) -> str:
    """Return current weather for a city (mocked)."""
    return f"Sunny 28C in {city}"

llm = ChatAnthropic(model="claude-opus-4-8")
agent = create_react_agent(llm, tools=[get_weather])

# Always pass typed HumanMessage, never raw dict
result = agent.invoke({
    "messages": [HumanMessage(content="What is the weather in Manila?")]
})

for msg in result["messages"]:
    print(f"{type(msg).__name__}: {msg.content}")

Common mistakes with LangGraph message types

  • Appending a plain string as a tool result. Wrap in ToolMessage(content=..., tool_call_id=...).
  • Mixing OpenAI-style dicts with LangChain typed messages. Pick one format and stick with it inside the graph state.
  • Skipping the tool_call_id on ToolMessage. The converter needs it to link the reply back to the original tool_call.
  • Rebuilding message classes in your own module. If you subclass BaseMessage, keep the type attribute set correctly, or the converter falls back and raises TypeError.

Debugging checklist for the SDK/library error

Before diving into fixes, run through this diagnostic checklist. Nine times out of ten the answer surfaces here.

  1. Read the full traceback, not just the error message. The stack trace shows exactly which line and which call chain triggered the error. The last line names the immediate cause; earlier lines show how you got there.
  2. Add print or debug statements just before the failing line. Print the variable, its type, and its value. Nine out of ten error surprises come from the value being different from what you assumed.
  3. Check Python version compatibility. Errors sometimes result from APIs that changed between versions. Run your interpreter version check and compare against the library documentation for that version.
  4. Isolate the failing call in a minimal reproducer. Copy the failing line into a small standalone script with hardcoded inputs. If it fails there too, the bug is in your code. If not, something in your surrounding context is contributing.
  5. Search the exact error message. Include the class name and the specific text in your search. Chances are someone else hit the same issue and the fix is documented on Stack Overflow or the library’s GitHub issues.

Common causes for the SDK/library error

Most instances of this error trace back to one of these root causes:

  • Uninitialized or missing input. A variable was not populated before use, or the input source (file, API response, database row) did not contain the expected key or value.
  • Type mismatch. The code expected a specific type (dict, list, string) but received something different. Python’s dynamic typing means this often surfaces at runtime, not at compile time.
  • Version drift. The library API changed and your code assumes the old signature. Check the library’s changelog for breaking changes since the version you last used.
  • Race condition or ordering issue. Async or concurrent code sometimes tries to access data before it is ready. Add awaits, locks, or explicit ordering to fix.
  • Copy-paste from stale tutorial. Older tutorials may use APIs that no longer exist. Always check the official docs for the current version.

Testing and prevention

Preventing this class of error from recurring is more valuable than fixing it once. Build these habits into your workflow:

  • Write tests that trigger the error path. If your test suite hits the error scenario, catch and assert it. A well-written test prevents the same bug from returning.
  • Validate inputs at API boundaries. When data enters your code from external sources (HTTP requests, file uploads, database queries), validate structure and types immediately.
  • Use type hints and static analysis. Tools like mypy for Python or TypeScript for JavaScript catch many type mismatches before you run the code.
  • Log important state. Structured logging with context helps you debug production issues faster. Include enough context to reconstruct what happened.
  • Read the library changelog. Before upgrading a dependency, skim the changelog for breaking changes. Two minutes of reading saves an hour of debugging.

When to ask for help

Some errors are worth solving yourself for the learning. Others are worth asking about early. Ask for help when: the error blocks a customer-facing feature, you have spent an hour without progress, the error involves security or data integrity, or you are unsure whether your fix will introduce new bugs. Post to Stack Overflow with a minimal reproducer, or ask a senior developer on your team. Time boxes are your friend.

Frequently Asked Questions

Which message classes work with every LangChain chat model?

HumanMessage, AIMessage, SystemMessage, ToolMessage, and FunctionMessage. All chat model wrappers (Anthropic, OpenAI, Groq, Ollama) accept these five. FunctionMessage is legacy in 2026, prefer ToolMessage.

Can I mix OpenAI dict format with LangChain messages?

Not safely. LangChain and LangGraph expect their own typed classes. Convert dicts to typed messages at the graph boundary, then use only typed messages inside.

Why does add_messages not coerce dicts automatically?

Coercing hides typos and lossy conversions. LangGraph chose strict typing so the graph state remains predictable and downstream reducers stay safe.

Does ToolMessage need tool_call_id?

Yes. Without it, the converter cannot link the tool reply back to the originating tool_call in AIMessage.tool_calls. Downstream models will not see the tool result correctly.

Can I subclass HumanMessage for custom metadata?

Yes, and it works if you keep the type attribute set to “human”. Use additional_kwargs for arbitrary metadata instead of new fields to stay compatible with future SDK updates.

Adrian Mercurio

Full-Stack Developer at PIES IT Solution

Specializes in building complete capstone projects with full documentation. Strong background in PHP/MySQL development and database design. Has personally built and tested over 30 capstone-ready projects with ER diagrams, DFDs, and chapter-by-chapter thesis documentation.

Expertise: PHP, Laravel, Database Design, Capstone Projects, C#, C, C++, Python, AI Projects  ·  View all posts by Adrian Mercurio →

Leave a Comment