Repository: PrefectHQ/prefect

Issue:
DotDict isn't JSON-serializable
`DotDicts` *feel* like dicts, until you try to ship them as JSON:

```python
In [1]: import json

In [2]: from prefect.utilities.collections import DotDict

In [3]: json.dumps(DotDict(x=1, y=2))
```
Results in the following error:

```
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-3-b595d5c6cfdf> in <module>()
----> 1 json.dumps(DotDict(x=1, y=2))

/anaconda3/lib/python3.6/json/__init__.py in dumps(obj, skipkeys, ensure_ascii, check_circular, allow_nan, cls, indent, separators, default, sort_keys, **kw)
    229         cls is None and indent is None and separators is None and
    230         default is None and not sort_keys and not kw):
--> 231         return _default_encoder.encode(obj)
    232     if cls is None:
    233         cls = JSONEncoder

/anaconda3/lib/python3.6/json/encoder.py in encode(self, o)
    197         # exceptions aren't as detailed.  The list call should be roughly
    198         # equivalent to the PySequence_Fast that ''.join() would do.
--> 199         chunks = self.iterencode(o, _one_shot=True)
    200         if not isinstance(chunks, (list, tuple)):
    201             chunks = list(chunks)

/anaconda3/lib/python3.6/json/encoder.py in iterencode(self, o, _one_shot)
    255                 self.key_separator, self.item_separator, self.sort_keys,
    256                 self.skipkeys, _one_shot)
--> 257         return _iterencode(o, 0)
    258
    259 def _make_iterencode(markers, _default, _encoder, _indent, _floatstr,

/anaconda3/lib/python3.6/json/encoder.py in default(self, o)
    178         """
    179         raise TypeError("Object of type '%s' is not JSON serializable" %
--> 180                         o.__class__.__name__)
    181
    182     def encode(self, o):

TypeError: Object of type 'DotDict' is not JSON serializable
```


Discussion:
The tricky thing about messing with JSON by, for example, modifying the default encoder, is that some libraries use their own custom encoders (or reinstantiate the default encoder) and therefore won't pick up the modifications. I guess the wishlist item would be for Python to think of `DotDict` as being a `dict`
So the TLDR here is that there is no internal-to-`DotDict` way of doing this _without_ changing the call to `json.dumps()`.  I tried to hack it to force json to think it was a `dict` and even that didn't work:
```python-traceback
TypeError: Object of type 'dict' is not JSON serializable
```
It makes sense.

Perhaps as a convenience, we could add a `to_dict()` method that calls `as_nested_dict(self, dict)`?

Resolving patch:
diff --git a/src/prefect/utilities/collections.py b/src/prefect/utilities/collections.py
--- a/src/prefect/utilities/collections.py
+++ b/src/prefect/utilities/collections.py
@@ -92,8 +92,9 @@ def copy(self) -> "DotDict":
         """Creates and returns a shallow copy of the current DotDict"""
         return type(self)(self.__dict__.copy())
 
-    def __json__(self) -> dict:
-        return dict(self)
+    def to_dict(self) -> dict:
+        """Converts current `DotDict` (and any `DotDict`s contained within) to an appropriate nested dictionary."""
+        return as_nested_dict(self, dct_class=dict)
 
 
 class GraphQLResult(DotDict):


Test patch:
diff --git a/tests/utilities/test_collections.py b/tests/utilities/test_collections.py
--- a/tests/utilities/test_collections.py
+++ b/tests/utilities/test_collections.py
@@ -235,6 +235,14 @@ def test_dotdict_is_not_json_serializable_with_default_encoder(self):
         with pytest.raises(TypeError):
             json.dumps(DotDict(x=1))
 
+    def test_dotdict_to_dict(self):
+        d = DotDict(x=5, y=DotDict(z="zzz", qq=DotDict()))
+        assert d.to_dict() == {"x": 5, "y": {"z": "zzz", "qq": {}}}
+
+    def test_dotdict_to_dict_with_lists_of_dicts(self):
+        d = DotDict(x=5, y=DotDict(z=[DotDict(abc=10, qq=DotDict())]))
+        assert d.to_dict() == {"x": 5, "y": {"z": [{"abc": 10, "qq": {}}]}}
+
 
 def test_as_nested_dict_defaults_dotdict():
     orig_d = dict(a=1, b=[2, dict(c=3)], d=dict(e=[dict(f=4)]))