deepset-ai/haystack · error

meta must be either None, a dictionary or a list of dictiona

Error message

meta must be either None, a dictionary or a list of dictionaries.

What it means

normalize_metadata only accepts None, a single dict, or a list of dicts as meta. Any other type (str, tuple, set, nested list, etc.) raises ValueError. This validates the type of the metadata argument before converters attach it to documents.

Source

Thrown at haystack/components/converters/utils.py:82

    makes sure to return a list of dictionaries of the correct length for the converter to use.

    :param meta: the meta input of the converter, as-is
    :param sources_count: the number of sources the converter received
    :returns: a list of dictionaries of the make length as the sources list

    Each source always gets its own independent dictionary. When ``meta`` is ``None`` or a single
    dictionary, a separate copy is returned for every source so that mutating one source's metadata
    downstream does not leak into the others.
    """
    if meta is None:
        return [{} for _ in range(sources_count)]
    if isinstance(meta, dict):
        return [deepcopy(meta) for _ in range(sources_count)]
    if isinstance(meta, list):
        if sources_count != len(meta):
            raise ValueError("The length of the metadata list must match the number of sources.")
        return meta
    raise ValueError("meta must be either None, a dictionary or a list of dictionaries.")

View on GitHub (pinned to e318778c9b)

Solutions

  1. Convert the input to a plain dict (json.loads for JSON strings) or list of dicts.
  2. Wrap a tuple/iterator in list(...) and each item in dict(...).
  3. Pass None if no metadata is needed.

Example fix

# before
converter.run(sources=srcs, meta='{"author": "x"}')  # str
# after
import json
converter.run(sources=srcs, meta=json.loads('{"author": "x"}'))
Defensive patterns

Strategy: type-guard

Validate before calling

def is_valid_meta(meta) -> bool:
    return meta is None or isinstance(meta, dict) or (
        isinstance(meta, list) and all(isinstance(m, dict) for m in meta)
    )

assert is_valid_meta(meta), f"meta must be None/dict/list-of-dicts, got {type(meta)}"

Type guard

def is_meta(meta: object) -> bool:
    if meta is None or isinstance(meta, dict):
        return True
    return isinstance(meta, list) and all(isinstance(m, dict) for m in meta)

Try / catch

try:
    result = converter.run(sources=sources, meta=meta)
except ValueError as e:
    if "meta must be" in str(e):
        meta = dict(json.loads(meta)) if isinstance(meta, str) else None
        result = converter.run(sources=sources, meta=meta)
    else:
        raise

Prevention

When it happens

Trigger: Calling a converter's run(..., meta=...) with a non-dict/list/None value such as a string, a tuple of dicts, or a generator.

Common situations: Passing a JSON string of metadata instead of a parsed dict; passing a tuple or pandas-derived structure; forgetting to deserialize metadata loaded from a file.

Understand the failure class

Background: Invalid argument type errors: "must be of type string", "expected X, got Y", and ERR_INVALID_ARG_TYPE explained — this error's family across 15 libraries.

Related errors


AI-assisted analysis of deepset-ai/haystack@e318778c9b (2026-08-30). Data as JSON: /api/errors/26e04eeedb6516ca. Report an issue: GitHub.