Repository: deepset-ai/haystack

Issue:
`ChatPromptBuilder` Fails to JSON Serialize due to `ChatMessage` class
**Describe the bug**
The new ChatPromptBuilder fails to serialize because of the ChatMessage dataclass. This component takes in a list of ChatMessages in the init method under the template

**Error message**
TypeError: Object of type ChatMessage is not JSON serializable

**Expected behavior**
The behavior I expect is that the component gets converted into JSON like this and that we can load with this JSON to create the pipeline back (I'm using a JSON Marshaller)

```
"components": {
    "ChatPromptBuilder": {
         "init_parameters": {
              "template": [
                   {
                        "content:" "test message"
                        "role": "user",
                        "name": "test"
                        "meta:" {
                            "test": true
                        }
                    }
              ]
         }
     }
}
```
**Additional context**
I noticed that ChatPromptBuilder and ChatMessage classes do not have any from dict or to dict functions, so it explains why they fail to serialize. In general, I've seen similar issues reported for DataClasses so I think they should all be serializable 

**To Reproduce**
```
    messages = [ChatMessage.from_system("You are a helpful assistant giving out valuable information to tourists."), ChatMessage.from_user("Tell me about {{location}}")]`
   chat_prompt_builder = ChatPromptBuilder(template=messages)
```


Discussion:
Thanks for reporting this.

**Reproducible example**
```python
from haystack.components.builders.chat_prompt_builder import ChatPromptBuilder
from haystack.dataclasses import ChatMessage
from haystack import Pipeline

messages = [ChatMessage.from_system("Message")]
chat_prompt_builder = ChatPromptBuilder(template=messages)

pipeline = Pipeline()
pipeline.add_component("builder", chat_prompt_builder)

pipe_str = pipeline.dumps()
new_pipeline = Pipeline().loads(pipe_str)
```
Let me try to solve this!
I been working on this for an hour now and I find that the problem here is that the 'component_to_dict' and 'deffault_to_dict' functions do not work recursively on fields that may need serialization, and just lives them as is. This makes serialization fail while we only use straightforward data types that should be serialized and deserialized without any problem.
In order to solve this specific bug we could just create the 'to_dict' and 'from_dict' methods of 'ChatPromptBuilder', but I believe that we could implement a process to serialize and deserialize instances recursively so we do not get this kind of errors again.
@CarlosFerLo go for the simple solution, custom `to_dict` and `from_dict` are the way to go.

Resolving patch:
diff --git a/haystack/components/builders/chat_prompt_builder.py b/haystack/components/builders/chat_prompt_builder.py
--- a/haystack/components/builders/chat_prompt_builder.py
+++ b/haystack/components/builders/chat_prompt_builder.py
@@ -6,7 +6,7 @@
 
 from jinja2 import Template, meta
 
-from haystack import component, logging
+from haystack import component, default_from_dict, default_to_dict, logging
 from haystack.dataclasses.chat_message import ChatMessage, ChatRole
 
 logger = logging.getLogger(__name__)
@@ -223,3 +223,36 @@ def _validate_variables(self, provided_variables: Set[str]):
                 f"Missing required input variables in ChatPromptBuilder: {missing_vars_str}. "
                 f"Required variables: {self.required_variables}. Provided variables: {provided_variables}."
             )
+
+    def to_dict(self) -> Dict[str, Any]:
+        """
+        Returns a dictionary representation of the component.
+
+        :returns:
+            Serialized dictionary representation of the component.
+        """
+        if self.template is not None:
+            template = [m.to_dict() for m in self.template]
+        else:
+            template = None
+
+        return default_to_dict(
+            self, template=template, variables=self._variables, required_variables=self._required_variables
+        )
+
+    @classmethod
+    def from_dict(cls, data: Dict[str, Any]) -> "ChatPromptBuilder":
+        """
+        Deserialize this component from a dictionary.
+
+        :param data:
+            The dictionary to deserialize and create the component.
+
+        :returns:
+            The deserialized component.
+        """
+        init_parameters = data["init_parameters"]
+        template = init_parameters.get("template", [])
+        init_parameters["template"] = [ChatMessage.from_dict(d) for d in template]
+
+        return default_from_dict(cls, data)
diff --git a/haystack/dataclasses/chat_message.py b/haystack/dataclasses/chat_message.py
--- a/haystack/dataclasses/chat_message.py
+++ b/haystack/dataclasses/chat_message.py
@@ -2,7 +2,7 @@
 #
 # SPDX-License-Identifier: Apache-2.0
 
-from dataclasses import dataclass, field
+from dataclasses import asdict, dataclass, field
 from enum import Enum
 from typing import Any, Dict, Optional
 
@@ -99,3 +99,29 @@ def from_function(cls, content: str, name: str) -> "ChatMessage":
         :returns: A new ChatMessage instance.
         """
         return cls(content, ChatRole.FUNCTION, name)
+
+    def to_dict(self) -> Dict[str, Any]:
+        """
+        Converts ChatMessage into a dictionary.
+
+        :returns:
+            Serialized version of the object.
+        """
+        data = asdict(self)
+        data["role"] = self.role.value
+
+        return data
+
+    @classmethod
+    def from_dict(cls, data: Dict[str, Any]) -> "ChatMessage":
+        """
+        Creates a new ChatMessage object from a dictionary.
+
+        :param data:
+            The dictionary to build the ChatMessage object.
+        :returns:
+            The created object.
+        """
+        data["role"] = ChatRole(data["role"])
+
+        return cls(**data)


Test patch:
diff --git a/test/components/builders/test_chat_prompt_builder.py b/test/components/builders/test_chat_prompt_builder.py
--- a/test/components/builders/test_chat_prompt_builder.py
+++ b/test/components/builders/test_chat_prompt_builder.py
@@ -494,3 +494,44 @@ def run(self):
                 ]
             }
         }
+
+    def test_to_dict(self):
+        component = ChatPromptBuilder(
+            template=[ChatMessage.from_user("text and {var}"), ChatMessage.from_assistant("content {required_var}")],
+            variables=["var", "required_var"],
+            required_variables=["required_var"],
+        )
+
+        assert component.to_dict() == {
+            "type": "haystack.components.builders.chat_prompt_builder.ChatPromptBuilder",
+            "init_parameters": {
+                "template": [
+                    {"content": "text and {var}", "role": "user", "name": None, "meta": {}},
+                    {"content": "content {required_var}", "role": "assistant", "name": None, "meta": {}},
+                ],
+                "variables": ["var", "required_var"],
+                "required_variables": ["required_var"],
+            },
+        }
+
+    def test_from_dict(self):
+        component = ChatPromptBuilder.from_dict(
+            data={
+                "type": "haystack.components.builders.chat_prompt_builder.ChatPromptBuilder",
+                "init_parameters": {
+                    "template": [
+                        {"content": "text and {var}", "role": "user", "name": None, "meta": {}},
+                        {"content": "content {required_var}", "role": "assistant", "name": None, "meta": {}},
+                    ],
+                    "variables": ["var", "required_var"],
+                    "required_variables": ["required_var"],
+                },
+            }
+        )
+
+        assert component.template == [
+            ChatMessage.from_user("text and {var}"),
+            ChatMessage.from_assistant("content {required_var}"),
+        ]
+        assert component._variables == ["var", "required_var"]
+        assert component._required_variables == ["required_var"]
diff --git a/test/dataclasses/test_chat_message.py b/test/dataclasses/test_chat_message.py
--- a/test/dataclasses/test_chat_message.py
+++ b/test/dataclasses/test_chat_message.py
@@ -81,3 +81,22 @@ def test_apply_custom_chat_templating_on_chat_message():
         formatted_messages, chat_template=anthropic_template, tokenize=False
     )
     assert tokenized_messages == "You are good assistant\nHuman: I have a question\nAssistant:"
+
+
+def test_to_dict():
+    message = ChatMessage.from_user("content")
+    message.meta["some"] = "some"
+
+    assert message.to_dict() == {"content": "content", "role": "user", "name": None, "meta": {"some": "some"}}
+
+
+def test_from_dict():
+    assert ChatMessage.from_dict(data={"content": "text", "role": "user", "name": None}) == ChatMessage(
+        content="text", role=ChatRole("user"), name=None, meta={}
+    )
+
+
+def test_from_dict_with_meta():
+    assert ChatMessage.from_dict(
+        data={"content": "text", "role": "user", "name": None, "meta": {"something": "something"}}
+    ) == ChatMessage(content="text", role=ChatRole("user"), name=None, meta={"something": "something"})