Repository: griptape-ai/griptape

Issue:
Can't Serialize Tool With Task Memory
```python
from griptape.memory.task.task_memory import TaskMemory
from griptape.tools import CalculatorTool

print(CalculatorTool(output_memory={"calculate": [TaskMemory()]}))
```

```
Traceback (most recent call last):
  File "/Users/collindutter/Documents/griptape/griptape-playground/src/misc/task_memory_serialization.py", line 4, in <module>
    print(CalculatorTool(output_memory={"calculate": [TaskMemory()]}))
  File "/Users/collindutter/Documents/griptape/griptape/griptape/mixins/serializable_mixin.py", line 96, in __str__
    return json.dumps(self.to_dict())
           ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/collindutter/.local/share/mise/installs/python/3.12.9/lib/python3.12/json/__init__.py", line 231, in dumps
    return _default_encoder.encode(obj)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/collindutter/.local/share/mise/installs/python/3.12.9/lib/python3.12/json/encoder.py", line 200, in encode
    chunks = self.iterencode(o, _one_shot=True)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/collindutter/.local/share/mise/installs/python/3.12.9/lib/python3.12/json/encoder.py", line 258, in iterencode
    return _iterencode(o, 0)
           ^^^^^^^^^^^^^^^^^
  File "/Users/collindutter/.local/share/mise/installs/python/3.12.9/lib/python3.12/json/encoder.py", line 180, in default
    raise TypeError(f'Object of type {o.__class__.__name__} '
TypeError: Object of type TaskMemory is not JSON serializable
```

Discussion:

Solution patch:
diff --git a/griptape/mixins/serializable_mixin.py b/griptape/mixins/serializable_mixin.py
index ade4dc68..0feeefff 100644
--- a/griptape/mixins/serializable_mixin.py
+++ b/griptape/mixins/serializable_mixin.py
@@ -3,7 +3,8 @@ from __future__ import annotations
 import json
 from abc import ABC
 from importlib import import_module
-from typing import TYPE_CHECKING, Generic, Optional, TypeVar, cast
+from json import JSONEncoder
+from typing import TYPE_CHECKING, Any, Generic, Optional, TypeVar, cast
 
 from attrs import Factory, define, field
 
@@ -15,6 +16,16 @@ if TYPE_CHECKING:
 T = TypeVar("T", bound="SerializableMixin")
 
 
+def _default(_self: Any, obj: Any) -> Any:
+    """Fallback method for JSONEncoder to handle custom serialization."""
+    return getattr(obj.__class__, "to_dict", getattr(_default, "default"))(obj)
+
+
+# Adapted from https://stackoverflow.com/questions/18478287/making-object-json-serializable-with-regular-encoder/18561055#18561055
+setattr(_default, "default", JSONEncoder.default)
+setattr(JSONEncoder, "default", _default)
+
+
 @define(slots=False)
 class SerializableMixin(Generic[T]):
     type: str = field(


Test patch:
diff --git a/tests/mocks/mock_serializable.py b/tests/mocks/mock_serializable.py
index b66142e1..028fc06b 100644
--- a/tests/mocks/mock_serializable.py
+++ b/tests/mocks/mock_serializable.py
@@ -25,3 +25,4 @@ class MockSerializable(SerializableMixin):
         default=None, kw_only=True, metadata={"serializable": True}
     )
     model: Optional[BaseModel] = field(default=None, kw_only=True, metadata={"serializable": True})
+    buzz: Optional[dict[str, MockSerializable]] = field(default=None, kw_only=True, metadata={"serializable": True})
diff --git a/tests/unit/mixins/test_seriliazable_mixin.py b/tests/unit/mixins/test_seriliazable_mixin.py
index 974dde02..38cdb137 100644
--- a/tests/unit/mixins/test_seriliazable_mixin.py
+++ b/tests/unit/mixins/test_seriliazable_mixin.py
@@ -37,8 +37,26 @@ class TestSerializableMixin:
         assert isinstance(TextArtifact.from_json('{"value": "foobar"}'), TextArtifact)
 
     def test_str(self):
-        assert str(MockSerializable()) == json.dumps(
-            {"type": "MockSerializable", "foo": "bar", "bar": None, "baz": None, "nested": None, "model": None}
+        assert str(MockSerializable(buzz={"foo": MockSerializable()})) == json.dumps(
+            {
+                "type": "MockSerializable",
+                "foo": "bar",
+                "bar": None,
+                "baz": None,
+                "nested": None,
+                "model": None,
+                "buzz": {
+                    "foo": {
+                        "type": "MockSerializable",
+                        "foo": "bar",
+                        "bar": None,
+                        "baz": None,
+                        "nested": None,
+                        "model": None,
+                        "buzz": None,
+                    }
+                },
+            }
         )
 
     def test_to_json(self):
@@ -50,6 +68,7 @@ class TestSerializableMixin:
                 "baz": None,
                 "nested": None,
                 "model": {"foo": "bar"},
+                "buzz": None,
             }
         )
 
@@ -61,6 +80,7 @@ class TestSerializableMixin:
             "baz": None,
             "nested": None,
             "model": {"foo": "bar"},
+            "buzz": None,
         }
 
     def test_import_class_rec(self):