Repository: griptape-ai/griptape (language: python)

Issue:
Add `JsonArtifact`
 - [x] I have read and agree to the [contributing guidelines](https://github.com/griptape-ai/griptape#contributing).

**Is your feature request related to a problem? Please describe.**
I want a way to pass around json-serialized values without having to explicitly de/serialize the dict myself

**Describe the solution you'd like**
`JsonArtifact` that takes a dict and serializes to json.

**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.

**Additional context**
Add any other context or screenshots about the feature request here.



Discussion / hints:
Related: https://github.com/griptape-ai/griptape/issues/439

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ca7c663ec..6d6e9bffc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Global config, `griptape.config.config`, for setting global configuration defaults.
 - Unique name generation for all `RagEngine` modules.
 - Support for bitshift composition in `BaseTask` for adding parent/child tasks.
+- `JsonArtifact` for handling de/seralization of values.
 
 ### Changed
 - **BREAKING**: Removed all uses of `EventPublisherMixin` in favor of `event_bus`.
diff --git a/Makefile b/Makefile
index 73175b7c5..1db428b2c 100644
--- a/Makefile
+++ b/Makefile
@@ -23,6 +23,7 @@ install/core: ## Install core dependencies.
 .PHONY: install/all
 install/all: ## Install all dependencies.
 	@poetry install --with dev --with test --with docs --all-extras
+	@poetry run pre-commit install
 
 .PHONY: install/dev
 install/dev: ## Install dev dependencies.
diff --git a/docs/griptape-framework/data/artifacts.md b/docs/griptape-framework/data/artifacts.md
index 065e36123..1a33a991b 100644
--- a/docs/griptape-framework/data/artifacts.md
+++ b/docs/griptape-framework/data/artifacts.md
@@ -58,3 +58,7 @@ A [BooleanArtifact](../../reference/griptape/artifacts/boolean_artifact.md) is u
 A [GenericArtifact](../../reference/griptape/artifacts/generic_artifact.md) can be used as an escape hatch for passing any type of data around the framework.
 It is generally not recommended to use this Artifact type, but it can be used in a handful of situations where no other Artifact type fits the data being passed.
 See [talking to a video](../../examples/talk-to-a-video.md) for an example of using a `GenericArtifact` to pass a Gemini-specific video file.
+
+## Json
+
+A [JsonArtifact](../../reference/griptape/artifacts/json_artifact.md) is used for passing JSON-serliazable data around the framework. Anything passed to `value` will be converted using `json.dumps(json.loads(value))`.
diff --git a/griptape/artifacts/__init__.py b/griptape/artifacts/__init__.py
index e9fc6daba..f39bfea8d 100644
--- a/griptape/artifacts/__init__.py
+++ b/griptape/artifacts/__init__.py
@@ -2,6 +2,7 @@
 from .error_artifact import ErrorArtifact
 from .info_artifact import InfoArtifact
 from .text_artifact import TextArtifact
+from .json_artifact import JsonArtifact
 from .blob_artifact import BlobArtifact
 from .boolean_artifact import BooleanArtifact
 from .csv_row_artifact import CsvRowArtifact
@@ -18,6 +19,7 @@
     "ErrorArtifact",
     "InfoArtifact",
     "TextArtifact",
+    "JsonArtifact",
     "BlobArtifact",
     "BooleanArtifact",
     "CsvRowArtifact",
diff --git a/griptape/artifacts/json_artifact.py b/griptape/artifacts/json_artifact.py
new file mode 100644
index 000000000..b292879a9
--- /dev/null
+++ b/griptape/artifacts/json_artifact.py
@@ -0,0 +1,21 @@
+from __future__ import annotations
+
+import json
+from typing import Union
+
+from attrs import define, field
+
+from griptape.artifacts import BaseArtifact
+
+Json = Union[dict[str, "Json"], list["Json"], str, int, float, bool, None]
+
+
+@define
+class JsonArtifact(BaseArtifact):
+    value: Json = field(converter=lambda v: json.loads(json.dumps(v)), metadata={"serializable": True})
+
+    def to_text(self) -> str:
+        return json.dumps(self.value)
+
+    def __add__(self, other: BaseArtifact) -> JsonArtifact:
+        raise NotImplementedError


Test patch:
diff --git a/tests/unit/artifacts/test_json_artifact.py b/tests/unit/artifacts/test_json_artifact.py
new file mode 100644
index 000000000..06f5d6297
--- /dev/null
+++ b/tests/unit/artifacts/test_json_artifact.py
@@ -0,0 +1,29 @@
+import json
+
+import pytest
+
+from griptape.artifacts import JsonArtifact, TextArtifact
+
+
+class TestJsonArtifact:
+    def test_value_type_conversion(self):
+        assert JsonArtifact({"foo": "bar"}).value == json.loads(json.dumps({"foo": "bar"}))
+        assert JsonArtifact({"foo": 1}).value == json.loads(json.dumps({"foo": 1}))
+        assert JsonArtifact({"foo": 1.0}).value == json.loads(json.dumps({"foo": 1.0}))
+        assert JsonArtifact({"foo": True}).value == json.loads(json.dumps({"foo": True}))
+        assert JsonArtifact({"foo": None}).value == json.loads(json.dumps({"foo": None}))
+        assert JsonArtifact([{"foo": {"bar": "baz"}}]).value == json.loads(json.dumps([{"foo": {"bar": "baz"}}]))
+        assert JsonArtifact(None).value == json.loads(json.dumps(None))
+        assert JsonArtifact("foo").value == json.loads(json.dumps("foo"))
+
+    def test___add__(self):
+        with pytest.raises(NotImplementedError):
+            JsonArtifact({"foo": "bar"}) + TextArtifact("invalid json")
+
+    def test_to_text(self):
+        assert JsonArtifact({"foo": "bar"}).to_text() == json.dumps({"foo": "bar"})
+        assert JsonArtifact({"foo": 1}).to_text() == json.dumps({"foo": 1})
+        assert JsonArtifact({"foo": 1.0}).to_text() == json.dumps({"foo": 1.0})
+        assert JsonArtifact({"foo": True}).to_text() == json.dumps({"foo": True})
+        assert JsonArtifact({"foo": None}).to_text() == json.dumps({"foo": None})
+        assert JsonArtifact([{"foo": {"bar": "baz"}}]).to_text() == json.dumps([{"foo": {"bar": "baz"}}])