deepset-ai/haystack · critical · DeserializationError

Refusing to deserialize '{handle}': it resolves to an import

Error message

Refusing to deserialize '{handle}': it resolves to an import primitive that can load arbitrary modules (equivalent to the blocked builtin '__import__'), which is a gateway to code execution. If you trust the source of this data, load it with unsafe=True to bypass deserialization safety checks.

What it means

Deserialization refuses to restore a callable whose handle resolves to an import primitive (e.g. importlib.import_module or builtins.__import__) because such a callable can load arbitrary modules and execute code. Haystack blocks known dangerous callables when loading serialized pipelines. If you trust the data source, re-load with unsafe=True.

Source

Thrown at haystack/core/serialization_security.py:518

    Reject `resolved` if it is an import primitive that is unsafe to resolve from serialized data.

    Used by the callable-resolution path (`deserialize_callable`) as a companion to
    :func:`_check_not_denied_builtin`. It blocks the non-builtin import primitives in
    :data:`_DENIED_CALLABLE_OBJECTS` / :data:`_DENIED_CALLABLE_QUALNAMES` (e.g.
    `importlib.import_module`, `haystack.utils.type_serialization.thread_safe_import`), which are
    functionally equivalent to the already-denied builtin `__import__` and can load any module as a
    gateway to code execution. Bypassed in `unsafe=True` mode, which disables all safety checks.

    :param resolved:
        The object resolved from the serialized handle.
    :param handle:
        The original serialized handle, used only for the error message.
    """
    if _is_unsafe_deserialization():
        return
    ident = (getattr(resolved, "__module__", ""), getattr(resolved, "__qualname__", ""))
    if any(resolved is denied for denied in _DENIED_CALLABLE_OBJECTS) or ident in _DENIED_CALLABLE_QUALNAMES:
        raise DeserializationError(
            f"Refusing to deserialize '{handle}': it resolves to an import primitive that can load "
            f"arbitrary modules (equivalent to the blocked builtin '__import__'), which is a gateway "
            f"to code execution. If you trust the source of this data, load it with unsafe=True to "
            f"bypass deserialization safety checks."
        )


def _check_builtin_is_type(resolved: object, handle: str) -> None:
    """
    Reject a `builtins` member resolved in a type/class context that is not a `type`.

    Used by `deserialize_type` and `import_class_by_name`, which resolve type annotations and class
    references — always classes. Requiring the resolved `builtins` member to be a `type` lets every
    builtin type through (e.g. `str`, `memoryview`) while rejecting every builtin *function* (e.g.
    `eval`, `exec`, `getattr`), with no denylist to maintain. Bypassed in `unsafe=True` mode.

    :param resolved:
        The object resolved from the serialized handle.

View on GitHub (pinned to e318778c9b)

Solutions

  1. Inspect the serialized handle and replace the import-primitive reference with a direct reference to the class/function you actually need
  2. If the data source is trusted, load with deserializer.loads(data, unsafe=True)
  3. Remove or sanitize the offending entry in the serialized file

Example fix

// before: serialized handle references an import primitive
"init_parameters": {"type": "importlib.import_module", ...}
// after: reference the concrete class directly, or load trusted data unsafely
"init_parameters": {"type": "haystack.components.builders.prompt_builder.PromptBuilder"}
data = Pipeline.loads(yaml_str, unsafe=True)  # only if source is trusted
Defensive patterns

Strategy: validation

Validate before calling

import re
DENIED = ("importlib.import_module", "builtins.__import__", "__builtin__.__import__")
def uses_import_primitive(serialized_yaml: str) -> bool:
    return any(h in serialized_yaml for h in DENIED)

Type guard

def is_safe_handle(handle: str) -> bool:
    root = handle.split(".")[0]
    return root not in {"importlib", "builtins", "__builtin__", "os", "subprocess"}

Try / catch

from haystack.core.errors import DeserializationError
try:
    pipe = Pipeline.loads(yaml_str)
except DeserializationError as e:
    if "import primitive" in str(e):
        pipe = Pipeline.loads(yaml_str, unsafe=True)  # only for trusted sources
    else:
        raise

Prevention

When it happens

Trigger: Deserializing pipeline YAML/JSON whose callable handle (e.g. an init_parameters entry) resolves via deserialize_callable to an object in _DENIED_CALLABLE_OBJECTS or a denied (module, qualname) pair such as importlib.__init__.import_module.

Common situations: Hand-edited or third-party serialized pipeline files that reference import functions; pipelines exported from older/other versions; tampered or untrusted pipeline payloads.

Related errors


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