Skip to content

deserialize_secrets_inplace with recursive=True stops restoring the keys you listed #12809

Description

@Lesereingrape

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions