deepset-ai / deepset-ai/haystack

deserialize_secrets_inplace with recursive=True stops restoring the keys you listed

Open
#12,809 0 comments 0 reactions 1 assignee View on GitHub

@davidsbatista is already working on this.

Since Sep 19, 2026.

Dominant language
Python
Stars
26.6k
Forks
3.2k
Avg merge
1d 3h
Merged PRs (30d)
194

Description

Description

deserialize_secrets_inplace(..., recursive=True) stops deserializing the keys you asked for: the values stay plain dict instead of becoming Secret, and nested dictionaries below one level are never visited. Turning the flag on therefore makes the helper do less than leaving it off.

haystack/utils/auth.py:234-238

for k, v in data.items():
    if isinstance(v, dict) and recursive:
        deserialize_secrets_inplace(v, keys)
    elif k in keys and v is not None:
        data[k] = Secret.from_dict(v)

A serialized Secret is a dict — Secret.from_env_var("MY_KEY").to_dict() returns {'type': 'env_var', 'env_vars': ['MY_KEY'], 'strict': True} — so with recursive=True the first branch claims every listed key and the Secret.from_dict branch becomes unreachable for exactly the inputs it exists to handle. The recursive call also drops the recursive argument, so it falls back to the default False and the traversal stops one level down.

Reproduction
from haystack.utils import Secret, deserialize_secrets_inplace

def probe():
    return Secret.from_env_var("MY_KEY").to_dict()

d = {"api_key": probe()}
deserialize_secrets_inplace(d, ["api_key"])
print(type(d["api_key"]).__name__)                      # EnvVarSecret

d = {"api_key": probe()}
deserialize_secrets_inplace(d, ["api_key"], recursive=True)
print(type(d["api_key"]).__name__)                      # dict  <-- listed key not restored

d = {"a": {"b": {"api_key": probe()}}}
deserialize_secrets_inplace(d, ["api_key"], recursive=True)
print(type(d["a"]["b"]["api_key"]).__name__)            # dict  <-- depth 3 never visited

Measured with Haystack 3.2.0-rc0 (current main @ b717d00), Python 3.13 on Windows; nothing else needed (no env var has to exist because to_dict()/from_dict() do not resolve values).

Expected behavior

recursive=True should be a strict superset of recursive=False: the keys named in keys get restored to Secret, and unlisted dict values get descended into at any depth. The docstring already promises that — :param recursive: Whether to recursively deserialize nested dictionaries. — and this is the documented way to rebuild secrets in a custom component's from_dict (docs-website/docs/concepts/secret-management.mdx).

Impact

deserialize_secrets_inplace is exported from haystack.utils and is the helper the docs recommend for custom components. A component that calls it with recursive=True silently keeps dict where warm_up()/run() expect a Secret, and the failure surfaces much later as AttributeError: 'dict' object has no attribute 'resolve_value', far from the cause.

No internal caller in haystack/ passes recursive=True, and test/utils/test_auth.py did not cover this helper at all, so I don't think anything depends on the current behavior — but that also means it was untested and easy to regress.

Possible solution

Check the requested keys first and forward the flag when descending (a few lines, same function). I'll open a PR with it plus the regression tests, referencing this issue.

Additional context

Reported as far as I can tell by reading the source; happy to be corrected if the current ordering is intentional.

Authored with an AI coding assistant; per CONTRIBUTING.md the AI-assistance disclaimer is in my PR description as well.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.