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.
Description
deserialize_secrets_inplace(..., recursive=True)stops deserializing the keys you asked for: the values stay plaindictinstead of becomingSecret, 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-238A serialized
Secretis a dict —Secret.from_env_var("MY_KEY").to_dict()returns{'type': 'env_var', 'env_vars': ['MY_KEY'], 'strict': True}— so withrecursive=Truethe first branch claims every listed key and theSecret.from_dictbranch becomes unreachable for exactly the inputs it exists to handle. The recursive call also drops therecursiveargument, so it falls back to the defaultFalseand the traversal stops one level down.Reproduction
Measured with Haystack
3.2.0-rc0(currentmain@b717d00), Python 3.13 on Windows; nothing else needed (no env var has to exist becauseto_dict()/from_dict()do not resolve values).Expected behavior
recursive=Trueshould be a strict superset ofrecursive=False: the keys named inkeysget restored toSecret, 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'sfrom_dict(docs-website/docs/concepts/secret-management.mdx).Impact
deserialize_secrets_inplaceis exported fromhaystack.utilsand is the helper the docs recommend for custom components. A component that calls it withrecursive=Truesilently keepsdictwherewarm_up()/run()expect aSecret, and the failure surfaces much later asAttributeError: 'dict' object has no attribute 'resolve_value', far from the cause.No internal caller in
haystack/passesrecursive=True, andtest/utils/test_auth.pydid 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.mdthe AI-assistance disclaimer is in my PR description as well.