Repository: deepset-ai/haystack

Issue:
`PyPDFToDocument` 2.0 is not JSON-serializable due to its `DefaultConverter`
**Describe the bug**
Pipelines containing a PyPDFConverter are not JSON-serializable due to the `DefaultConverter` set in its init. 

**Error message**
`TypeError: Object of type DefaultConverter is not JSON serializable`

**Expected behavior**
JSON-serializing and deserializing a pipeline containing a PyPDFConverter should be possible. 

**Additional context**
Similarly, the following should be possible:
```python
from haystack.preview.components.file_converters import PyPDFToDocument
from haystack.preview import Pipeline
p = Pipeline()
p.add_component(instance=PyPDFToDocument(), name="pdf_file_converter")
x = Pipeline.loads(p.dumps())
```
but it currently raises:
```
yaml.constructor.ConstructorError: could not determine a constructor for the tag 'tag:yaml.org,2002:python/object:haystack.preview.components.file_converters.pypdf.DefaultConverter'
  in "<unicode string>", line 4, column 18:
          converter: !!python/object:haystack.preview ...
```

**To Reproduce**
```python
from haystack.preview.components.file_converters import PyPDFToDocument
from haystack.preview import Pipeline
import json
p = Pipeline()
p.add_component(instance=PyPDFToDocument(), name="pdf_file_converter")
print(json.dumps(p.to_dict(), indent=4))

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/opt/homebrew/Caskroom/miniforge/base/envs/py310-Oct/lib/python3.10/json/__init__.py", line 238, in dumps
    **kw).encode(obj)
  File "/opt/homebrew/Caskroom/miniforge/base/envs/py310-Oct/lib/python3.10/json/encoder.py", line 201, in encode
    chunks = list(chunks)
  File "/opt/homebrew/Caskroom/miniforge/base/envs/py310-Oct/lib/python3.10/json/encoder.py", line 431, in _iterencode
    yield from _iterencode_dict(o, _current_indent_level)
  File "/opt/homebrew/Caskroom/miniforge/base/envs/py310-Oct/lib/python3.10/json/encoder.py", line 405, in _iterencode_dict
    yield from chunks
  File "/opt/homebrew/Caskroom/miniforge/base/envs/py310-Oct/lib/python3.10/json/encoder.py", line 405, in _iterencode_dict
    yield from chunks
  File "/opt/homebrew/Caskroom/miniforge/base/envs/py310-Oct/lib/python3.10/json/encoder.py", line 405, in _iterencode_dict
    yield from chunks
  [Previous line repeated 1 more time]
  File "/opt/homebrew/Caskroom/miniforge/base/envs/py310-Oct/lib/python3.10/json/encoder.py", line 438, in _iterencode
    o = _default(o)
  File "/opt/homebrew/Caskroom/miniforge/base/envs/py310-Oct/lib/python3.10/json/encoder.py", line 179, in default
    raise TypeError(f'Object of type {o.__class__.__name__} '
TypeError: Object of type DefaultConverter is not JSON serializable
```

**FAQ Check**
- [ ] Have you had a look at [our new FAQ page](https://docs.haystack.deepset.ai/docs/faq)?

**System:**
 - OS:
 - GPU/CPU:
 - Haystack version (commit or version number):
 - DocumentStore:
 - Reader:
 - Retriever:



Discussion:

Resolving patch:
diff --git a/haystack/preview/components/converters/pypdf.py b/haystack/preview/components/converters/pypdf.py
--- a/haystack/preview/components/converters/pypdf.py
+++ b/haystack/preview/components/converters/pypdf.py
@@ -1,11 +1,11 @@
 import io
 import logging
-from typing import List, Union, Optional, Protocol
+from typing import List, Union, Protocol, Dict
 from pathlib import Path
 
 from haystack.preview.dataclasses import ByteStream
 from haystack.preview.lazy_imports import LazyImport
-from haystack.preview import Document, component
+from haystack.preview import Document, component, default_to_dict
 
 with LazyImport("Run 'pip install pypdf'") as pypdf_import:
     from pypdf import PdfReader
@@ -34,6 +34,11 @@ def convert(self, reader: "PdfReader") -> Document:
         return Document(content=text)
 
 
+# This registry is used to store converters names and instances.
+# It can be used to register custom converters.
+CONVERTERS_REGISTRY: Dict[str, PyPDFConverter] = {"default": DefaultConverter()}
+
+
 @component
 class PyPDFToDocument:
     """
@@ -42,14 +47,27 @@ class PyPDFToDocument:
     A default text extraction converter is used if no custom converter is provided.
     """
 
-    def __init__(self, converter: Optional[PyPDFConverter] = None):
+    def __init__(self, converter_name: str = "default"):
         """
         Initializes the PyPDFToDocument component with an optional custom converter.
-        :param converter: A converter instance that adheres to the PyPDFConverter protocol.
-                          If None, the DefaultConverter is used.
+        :param converter_name: A converter name that is registered in the CONVERTERS_REGISTRY.
+            Defaults to 'default'.
         """
         pypdf_import.check()
-        self.converter: PyPDFConverter = converter or DefaultConverter()
+
+        try:
+            converter = CONVERTERS_REGISTRY[converter_name]
+        except KeyError:
+            msg = (
+                f"Invalid converter_name: {converter_name}.\n Available converters: {list(CONVERTERS_REGISTRY.keys())}"
+            )
+            raise ValueError(msg) from KeyError
+        self.converter_name = converter_name
+        self._converter: PyPDFConverter = converter
+
+    def to_dict(self):
+        # do not serialize the _converter instance
+        return default_to_dict(self, converter_name=self.converter_name)
 
     @component.output_types(documents=List[Document])
     def run(self, sources: List[Union[str, Path, ByteStream]]):
@@ -63,7 +81,7 @@ def run(self, sources: List[Union[str, Path, ByteStream]]):
         for source in sources:
             try:
                 pdf_reader = self._get_pdf_reader(source)
-                document = self.converter.convert(pdf_reader)
+                document = self._converter.convert(pdf_reader)
             except Exception as e:
                 logger.warning("Could not read %s and convert it to Document, skipping. %s", source, e)
                 continue


Test patch:
diff --git a/test/preview/components/converters/test_pypdf_to_document.py b/test/preview/components/converters/test_pypdf_to_document.py
--- a/test/preview/components/converters/test_pypdf_to_document.py
+++ b/test/preview/components/converters/test_pypdf_to_document.py
@@ -3,11 +3,20 @@
 from pypdf import PdfReader
 
 from haystack.preview import Document
-from haystack.preview.components.converters.pypdf import PyPDFToDocument
+from haystack.preview.components.converters.pypdf import PyPDFToDocument, CONVERTERS_REGISTRY
 from haystack.preview.dataclasses import ByteStream
 
 
 class TestPyPDFToDocument:
+    def test_init(self):
+        component = PyPDFToDocument()
+        assert component.converter_name == "default"
+        assert hasattr(component, "_converter")
+
+    def test_init_fail_nonexisting_converter(self):
+        with pytest.raises(ValueError):
+            PyPDFToDocument(converter_name="non_existing_converter")
+
     @pytest.mark.unit
     def test_run(self, preview_samples_path):
         """
@@ -58,7 +67,9 @@ class MyCustomConverter:
             def convert(self, reader: PdfReader) -> Document:
                 return Document(content="I don't care about converting given pdfs, I always return this")
 
-        converter = PyPDFToDocument(converter=MyCustomConverter())
+        CONVERTERS_REGISTRY["custom"] = MyCustomConverter()
+
+        converter = PyPDFToDocument(converter_name="custom")
         output = converter.run(sources=paths)
         docs = output["documents"]
         assert len(docs) == 1