deepset-ai / deepset-ai/haystack
deserialize_secrets_inplace with recursive=True stops restoring the keys you listed
@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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.