fix: stop ConditionalRouter and BranchJoiner from_dict from mutating the caller's data - #12935
Conversation
…the caller's data
|
@simpleqt is attempting to deploy a commit to the deepset Team on Vercel. A member of the Team first needs to authorize it. |
|
@simpleqt thanks for the contribution, can you please make the CI green, afterwards we can start a review |
- from_dict: only iterate routes when present (mypy union-attr) - test: annotate routes as list[Route] (mypy arg-type) - add releasenotes/notes entry (reno)
|
CI is green-able now (pushed): the |
Coverage reportClick to see where and how coverage changed
This report was generated by python-coverage-comment-action |
||||||||||||||||||||||||||||||||||||
- Leave custom_filters out of the init parameters when the serialized data has none, instead of passing an empty dict to __init__ - Build the deserialized custom_filters as a new dict rather than copying and then mutating it - Shorten the copy-rationale comments to one line - Add the reno hash suffix to the release note filename Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
julian-risch
left a comment
There was a problem hiding this comment.
Thanks @simpleqt ! Looks good to me! I made some smaller changes and the PR is ready to be merged now.
if you're curious, reno release note had no hash suffix, when the data has no custom_filters, it passed custom_filters={} to init instead of leaving the key out, and I shortened the comments in the code.
Problem
ConditionalRouter.from_dictandBranchJoiner.from_dictwrite deserialization results back into the caller's dict:The core framework already established this is harmful —
haystack/core/serialization.pydeliberately copiesinit_parametersfirst, with a comment: "Copy so that replacing serialized sub-objects ... does not mutate the caller'sdatadict in place. Without this, a second deserialization of the same dict would receive already-parsed objects." These two components bypass that protection.Consequence — loading the same serialized dict twice (a common "load pipeline definition once, rebuild components per request" pattern) crashes:
and after the first
from_dictthe serialized data is corrupted (d["init_parameters"]["routes"][0]["output_type"]is atypeobject, not the serialized string).Fix
Apply the same copy discipline the core serializer uses, in both components: shallow-copy
init_parameters(and the route dicts / custom filters map) before replacing serialized values with parsed ones, and pass the copy todefault_from_dict. The caller's dict is left untouched and repeated deserialization works.Test plan
TestBranchJoinerDeserialization::test_from_dict_does_not_mutate_caller_dataTestConditionalRouterDeserialization::test_from_dict_does_not_mutate_caller_dataBoth assert the serialized dict is unchanged after
from_dictand that a secondfrom_dictsucceeds. Both fail onmainand pass with this change (red/green verified). All 50 pre-existing tests in the two files still pass; ruff check + format (0.16.0) clean.