{"record":{"id":"b3efbe1e07ad764c","repo":"deepset-ai/haystack","slug":"the-insert-expression-must-evaluate-to-a-c","errorCode":null,"errorMessage":"The '{% insert %}' expression must evaluate to a ChatMessage or a list of ChatMessage objects. Got: {type(messages).__name__}.","messagePattern":"The '(.+?)' expression must evaluate to a ChatMessage or a list of ChatMessage objects\\. Got: (.+?)\\.","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"haystack/utils/jinja2_chat_extension.py","lineNumber":278,"sourceCode":"        as `_build_chat_message_json`, so the messages are parsed back into ChatMessage objects by the\n        ChatPromptBuilder alongside any literal `{% message %}` blocks. The full `ChatMessage.to_dict()` payload is\n        serialized so that all content types (tool calls, tool call results, images, reasoning, name and meta) round\n        trip without loss.\n\n        :param messages: The value the `{% insert %}` expression evaluated to. A missing or empty value expands to\n            nothing. A single ChatMessage is also accepted, since indexing with an integer (for example\n            `{% insert messages[-1] %}`) yields one message rather than a list. The value is validated at render time\n            because it comes from untrusted template input.\n        :param caller: Callable that returns the (empty) rendered body. Unused.\n        :return: Newline-terminated JSON lines, one per message, or an empty string if there are no messages.\n        :raises ValueError: If the value is not a ChatMessage or a list of ChatMessage objects.\n        \"\"\"\n        if isinstance(messages, ChatMessage):\n            messages = [messages]\n        if not messages:\n            return \"\"\n        if not isinstance(messages, (list, tuple)) or not all(isinstance(m, ChatMessage) for m in messages):\n            raise ValueError(\n                \"The '{% insert %}' expression must evaluate to a ChatMessage or a list of ChatMessage objects. \"\n                f\"Got: {type(messages).__name__}.\"\n            )\n        return \"\".join(json.dumps(message.to_dict()) + \"\\n\" for message in messages)\n\n    @staticmethod\n    def _parse_content_parts(content: str, start_tag: str, end_tag: str) -> list[ChatMessageContentT]:\n        \"\"\"\n        Parse a string into a sequence of ChatMessageContentT objects.\n\n        This method handles:\n        - Plain text content, converted to TextContent objects\n        - Structured content parts wrapped in sentinel tags, converted to ChatMessageContentT objects\n\n        :param content: Input string containing mixed text and content parts\n        :param start_tag: The opening sentinel tag (including the nonce)\n        :param end_tag: The closing sentinel tag (including the nonce)\n        :return: A list of ChatMessageContentT objects","sourceCodeStart":260,"sourceCodeEnd":296,"githubUrl":"https://github.com/deepset-ai/haystack/blob/e318778c9bf60a1963e3b5f451359655dd696c30/haystack/utils/jinja2_chat_extension.py#L260-L296","documentation":"In Haystack's Jinja2 chat template extension, the `{% insert %}` expression must resolve to a ChatMessage or a list/tuple of ChatMessage objects. This error means the expression evaluated to some other type (e.g. str, dict, None after the empty check), so the extension cannot serialize it into message JSON for the rendered prompt.","triggerScenarios":"Using `{% insert %}` in a chat template with a template variable or expression that yields a plain string, dict, or other non-ChatMessage object instead of a ChatMessage or list of ChatMessages.","commonSituations":"Passing a raw string (e.g. a document's text) instead of a ChatMessage; assigning a list of strings; a pipeline variable typed wrongly and fed into the template; forgetting that the empty case is handled before this check.","solutions":["Wrap the value in a ChatMessage, e.g. ChatMessage.from_user(text) instead of a raw string.","If passing a list, ensure every element is a ChatMessage; convert with [ChatMessage.from_user(t) for t in items].","Check the pipeline component output connected to the template variable and verify its type annotation.","If the intent is plain text insertion, use normal Jinja2 interpolation {{ var }} instead of {% insert %}."],"exampleFix":"// before\nmessages = \"Hello world\"\n{% insert %}{{ messages }}{% endinsert %}\n// after\nmessages = [ChatMessage.from_user(\"Hello world\")]\n{% insert %}{{ messages }}{% endinsert %}","handlingStrategy":"type-guard","validationCode":"from haystack.dataclasses import ChatMessage\n\ndef validate_insert_value(v):\n    ok = isinstance(v, ChatMessage) or (isinstance(v, (list, tuple)) and all(isinstance(m, ChatMessage) for m in v))\n    if not ok:\n        raise TypeError(f\"{% insert %} value must be ChatMessage or list of ChatMessages, got {type(v).__name__}\")","typeGuard":"def is_insertable(v) -> bool:\n    return isinstance(v, ChatMessage) or (isinstance(v, (list, tuple)) and all(isinstance(m, ChatMessage) for m in v))","tryCatchPattern":"try:\n    rendered = renderer.run(template=tpl, variables=vars)\nexcept ValueError as e:\n    if \"must evaluate to a ChatMessage\" in str(e):\n        log.error(\"bad {% insert %} input: %s\", vars)\n    raise","preventionTips":["Type-annotate pipeline variables feeding templates as ChatMessage or list[ChatMessage].","Convert raw strings to ChatMessage.from_user before passing them to templates.","Use {{ var }} for plain-text interpolation instead of {% insert %}.","Add unit tests rendering every chat template with representative variables."],"tags":["jinja2","chat-template","type-mismatch"],"backgroundTag":"template-expression-type-mismatch","analyzedSha":"e318778c9bf60a1963e3b5f451359655dd696c30","analyzedAt":"2026-08-30T11:45:20.711Z","schemaVersion":2},"datasetVersion":"2026-08-30T13:17:10.514Z"}