Repository: deepset-ai/haystack

Issue:
ConditionalRouter to_dict breaks routes object
**Describe the bug**
The conditional router component is broken after to_dict is called because `self.routes` is being mutated for returning the dictionary. This is problematic because during pipeline run `self.routes` is then incorrect

https://github.com/deepset-ai/haystack/blob/main/haystack/components/routers/conditional_router.py#L246

**Error message**
`self.routes` route object `output_type` is corrupt after serialization, so the types do not match

**Expected behavior**
I expect the to_dict method to make a new routes array and return it instead of mutating existing one

Something like this
  
         serialized_routes = []
        for route in self.routes:
            # output_type needs to be serialized to a string
            serialized_routes.append({
                **route,
                'output_type': serialize_type(route["output_type"])
            })

**FAQ Check**
- [X ] Have you had a look at [our new FAQ page](https://docs.haystack.deepset.ai/docs/faq)?

Discussion:

Solution patch:
diff --git a/haystack/components/routers/conditional_router.py b/haystack/components/routers/conditional_router.py
index 5fbe399248..6a1a983a74 100644
--- a/haystack/components/routers/conditional_router.py
+++ b/haystack/components/routers/conditional_router.py
@@ -241,13 +241,14 @@ def to_dict(self) -> Dict[str, Any]:
         :returns:
             Dictionary with serialized data.
         """
+        serialized_routes = []
         for route in self.routes:
             # output_type needs to be serialized to a string
-            route["output_type"] = serialize_type(route["output_type"])
+            serialized_routes.append({**route, "output_type": serialize_type(route["output_type"])})
         se_filters = {name: serialize_callable(filter_func) for name, filter_func in self.custom_filters.items()}
         return default_to_dict(
             self,
-            routes=self.routes,
+            routes=serialized_routes,
             custom_filters=se_filters,
             unsafe=self._unsafe,
             validate_output_type=self._validate_output_type,
diff --git a/releasenotes/notes/fix-conditional-router-to-dict-5af887da50effe11.yaml b/releasenotes/notes/fix-conditional-router-to-dict-5af887da50effe11.yaml
new file mode 100644
index 0000000000..4640663b8b
--- /dev/null
+++ b/releasenotes/notes/fix-conditional-router-to-dict-5af887da50effe11.yaml
@@ -0,0 +1,5 @@
+---
+fixes:
+  - |
+    Fix a bug where the `output_type` of a `ConditionalRouter` was not being serialized correctly.
+    This would cause the router to work incorrectly after being serialized and deserialized.


Test patch:
diff --git a/test/components/routers/test_conditional_router.py b/test/components/routers/test_conditional_router.py
index 478e62d5bf..eef30f08cb 100644
--- a/test/components/routers/test_conditional_router.py
+++ b/test/components/routers/test_conditional_router.py
@@ -542,3 +542,35 @@ def test_warns_on_unused_optional_variables(self):
         # Verify router still works normally
         result = router.run(question="What?", mode="chat")
         assert result == {"chat": "What?"}
+
+    def test_router_to_dict_does_not_mutate_routes(self):
+        routes = [
+            {"condition": "{{streams|length < 2}}", "output": "{{query}}", "output_type": str, "output_name": "query"}
+        ]
+
+        router = ConditionalRouter(routes)
+
+        # Store the original output_type before serializing
+        original_output_type = router.routes[0]["output_type"]
+        assert original_output_type is str
+
+        router_dict = router.to_dict()
+
+        # Verify that the original routes are not mutated
+        assert router.routes[0]["output_type"] is str
+        assert router.routes[0]["output_type"] is original_output_type
+
+        # Verify that the serialized output_type is a string
+        assert isinstance(router_dict["init_parameters"]["routes"][0]["output_type"], str), (
+            "Serialized output_type should be a string"
+        )
+
+        # Verify that the router still works correctly after to_dict()
+        result = router.run(streams=[1], query="test")
+        assert result == {"query": "test"}, "Router should still work correctly after to_dict()"
+
+        # Double check on another ConditionalRouter instance
+        new_router = ConditionalRouter.from_dict(router_dict)
+        assert new_router.routes == router.routes
+        assert new_router.routes[0]["output_type"] is str
+        assert new_router.routes[0]["output_type"] is original_output_type