langchain-ai/langchain · error · ValueError

ToolMessage content should be a string or a list of string/d

Error message

ToolMessage content should be a string or a list of string/dicts. Received:\n\n{content=}\n\n which could not be coerced into a string.

What it means

Raised in ToolMessage's Pydantic validator when `content` is not a str or list and calling `str(content)` on it itself raises a ValueError. This only happens when the object has a broken `__str__`/`__repr__` that throws, so the failure points at the payload object, not at ToolMessage usage.

Source

Thrown at libs/core/langchain_core/messages/tool.py:112

        Args:
            values: The model arguments.

        """
        content = values["content"]
        if isinstance(content, tuple):
            content = list(content)

        if not isinstance(content, (str, list)):
            try:
                values["content"] = str(content)
            except ValueError as e:
                msg = (
                    "ToolMessage content should be a string or a list of string/dicts. "
                    f"Received:\n\n{content=}\n\n which could not be coerced into a "
                    "string."
                )
                raise ValueError(msg) from e
        elif isinstance(content, list):
            values["content"] = []
            for i, x in enumerate(content):
                if not isinstance(x, (str, dict)):
                    try:
                        values["content"].append(str(x))
                    except ValueError as e:
                        msg = (
                            "ToolMessage content should be a string or a list of "
                            "string/dicts. Received a list but "
                            f"element ToolMessage.content[{i}] is not a dict and could "
                            f"not be coerced to a string.:\n\n{x}"
                        )
                        raise ValueError(msg) from e
                else:
                    values["content"].append(x)

        tool_call_id = values["tool_call_id"]

View on GitHub (pinned to e32fa9a52e)

Solutions

  1. Serialize before constructing: pass `json.dumps(obj, default=str)` or `str(obj)` yourself in a context where you control errors
  2. Fix or relax the custom object's `__str__`/`__repr__` so it cannot raise
  3. Pass a list of dicts (`[{"result": data}]`) which ToolMessage accepts without coercion

Example fix

# before
tool_msg = ToolMessage(content=custom_obj, tool_call_id=call_id)  # custom_obj.__str__ raises

# after
import json
tool_msg = ToolMessage(content=json.dumps(custom_obj, default=str), tool_call_id=call_id)
Defensive patterns

Strategy: validation

Validate before calling

def coercible_content(content) -> bool:
    if isinstance(content, (str, list)):
        return True
    try:
        str(content)
    except Exception:
        return False
    return True

Type guard

def is_tool_message_safe_content(content) -> bool:
    if isinstance(content, str):
        return True
    if isinstance(content, list):
        return all(isinstance(x, (str, dict)) for x in content)
    return False

Try / catch

try:
    msg = ToolMessage(content=payload, tool_call_id=cid)
except ValueError:
    msg = ToolMessage(content=json.dumps(payload, default=str), tool_call_id=cid)

Prevention

When it happens

Trigger: Passing a custom object as ToolMessage content whose `__str__` raises ValueError (or raises inside `__repr__` used by `__str__`); objects whose string conversion depends on state that is missing at construction time.

Common situations: Wrapping SDK response objects or ORM rows in ToolMessage without first serializing them; custom data classes with strict `__str__` implementations that validate fields; partially-initialized objects escaping into message content.

Related errors


AI-assisted analysis of langchain-ai/langchain@e32fa9a52e (2026-08-14). Data as JSON: /api/errors/dd2cc3c47c36d6b6. Report an issue: GitHub.