Repository: evidentlyai/evidently
Issue: Unable to add DataStabilityTestPreset to workspace (datetime)
Root cause: Validation Gap

**Bug description:**
When trying to add a test suite to a local or remote workspace, both DataStabilityTestPreset and DataQualityTestPreset fail because of a JSON serialization error with datetime.

**Environment:** 
1. MacOS Ventura 13.5.2 (M2 Chip)
2. python 3.9

**Minimal reproducible example:**
```
from evidently.test_preset import (
    DataQualityTestPreset,
    DataStabilityTestPreset,
)
from evidently.test_suite import TestSuite
from evidently.ui.workspace import Workspace
from evidently import ColumnMapping
import pandas as pd
import datetime as datetime

ref = pd.DataFrame(
    {
        "my_target": [1, 2, 3],
        "prediction": [1, 2, 3],
        "feature1": [1, 2, 3],
        "feature2": ["a", "b", "c"],
        "datetime": [
            datetime.datetime(2023, 10, 1),
            datetime.datetime(2023, 11, 1),
            datetime.datetime(2023, 12, 1),
        ],
    }
)
curr = pd.DataFrame(
    {
        "my_target": [1, 2, 3],
        "prediction": [1, 2, 3],
        "feature1": [1.5, 2.5, 3.2],
        "feature2": ["c", "b", "a"],
        "datetime": [
            datetime.datetime(2023, 10, 1),
            datetime.datetime(2023, 11, 1),
            datetime.datetime(2023, 12, 1),
        ],
    }
)
test_suite = TestSuite(
    tests=[DataStabilityTestPreset(columns=["feature1", "feature2"])]
)
column_mapping = ColumnMapping(target="my_target", datetime="datetime")
test_suite.run(reference_data=ref, current_data=curr, column_mapping=column_mapping)
workspace = Workspace.create("workspace")
project = workspace.create_project("test_project")
workspace.add_test_suite(project.id, test_suite)
```
**Error shown below:**
```
TypeError                                 Traceback (most recent call last)
Cell In[109], line 1
----> 1 workspace.add_test_suite(project.id, test_suite)

File [~/miniconda3/lib/python3.9/site-packages/evidently/ui/workspace.py:248](https://file+.vscode-resource.vscode-cdn.net/Users/miniconda3/lib/python3.9/site-packages/evidently/ui/workspace.py:248), in WorkspaceBase.add_test_suite(self, project_id, test_suite)
    247 def add_test_suite(self, project_id: STR_UUID, test_suite: TestSuite):
--> 248     self.add_snapshot(project_id, test_suite.to_snapshot())

File [~/miniconda3/lib/python3.9/site-packages/evidently/ui/workspace.py:309](https://file+.vscode-resource.vscode-cdn.net/Users/examples/miniconda3/lib/python3.9/site-packages/evidently/ui/workspace.py:309), in Workspace.add_snapshot(self, project_id, snapshot)
    307 if isinstance(project_id, str):
    308     project_id = uuid.UUID(project_id)
--> 309 self._projects[project_id].add_snapshot(snapshot)

File [~/miniconda3/lib/python3.9/site-packages/evidently/ui/workspace.py:125](https://file+.vscode-resource.vscode-cdn.net/Users/miniconda3/lib/python3.9/site-packages/evidently/ui/workspace.py:125), in Project.add_snapshot(self, snapshot)
    123 def add_snapshot(self, snapshot: Snapshot):
    124     item = ProjectSnapshot(snapshot.id, self, snapshot)
--> 125     snapshot.save(item.path)
    126     self._snapshots[item.id] = item

File [~/miniconda3/lib/python3.9/site-packages/evidently/suite/base_suite.py:449](https://file+.vscode-resource.vscode-cdn.net/Users/miniconda3/lib/python3.9/site-packages/evidently/suite/base_suite.py:449), in save(self, filename)
    437     except BaseException as ex:
    438         test_results[test] = TestResult(
    439             name=test.name,
    440             status=TestStatus.ERROR,
   (...)
    444             exception=ex,
    445         )
    446     test_results[test].groups.update(
    447         {
    448             GroupingTypes.TestGroup.id: test.group,
--> 449             GroupingTypes.TestType.id: test.name,
    450         }
    451     )
    453 self.context.test_results = test_results
    454 self.context.state = States.Tested

File [~/miniconda3/lib/python3.9/json/__init__.py:179](https://file+.vscode-resource.vscode-cdn.net/Users/miniconda3/lib/python3.9/json/__init__.py:179), in dump(obj, fp, skipkeys, ensure_ascii, check_circular, allow_nan, cls, indent, separators, default, sort_keys, **kw)
    173     iterable = cls(skipkeys=skipkeys, ensure_ascii=ensure_ascii,
    174         check_circular=check_circular, allow_nan=allow_nan, indent=indent,
    175         separators=separators,
    176         default=default, sort_keys=sort_keys, **kw).iterencode(obj)
    177 # could accelerate with writelines in some versions of Python, at
    178 # a debuggability cost
--> 179 for chunk in iterable:
    180     fp.write(chunk)

File [~/miniconda3/lib/python3.9/json/encoder.py:431](https://file+.vscode-resource.vscode-cdn.net/Users/miniconda3/lib/python3.9/json/encoder.py:431), in _make_iterencode.<locals>._iterencode(o, _current_indent_level)
    429     yield from _iterencode_list(o, _current_indent_level)
    430 elif isinstance(o, dict):
--> 431     yield from _iterencode_dict(o, _current_indent_level)
    432 else:
    433     if markers is not None:

File [~/miniconda3/lib/python3.9/json/encoder.py:405](https://file+.vscode-resource.vscode-cdn.net/Users/miniconda3/lib/python3.9/json/encoder.py:405), in _make_iterencode.<locals>._iterencode_dict(dct, _current_indent_level)
    403         else:
    404             chunks = _iterencode(value, _current_indent_level)
--> 405         yield from chunks
    406 if newline_indent is not None:
    407     _current_indent_level -= 1

File [~/miniconda3/lib/python3.9/json/encoder.py:405](https://file+.vscode-resource.vscode-cdn.net/Users/miniconda3/lib/python3.9/json/encoder.py:405), in _make_iterencode.<locals>._iterencode_dict(dct, _current_indent_level)
    403         else:
    404             chunks = _iterencode(value, _current_indent_level)
--> 405         yield from chunks
    406 if newline_indent is not None:
    407     _current_indent_level -= 1

File [~/miniconda3/lib/python3.9/json/encoder.py:325](https://file+.vscode-resource.vscode-cdn.net/Users/miniconda3/lib/python3.9/json/encoder.py:325), in _make_iterencode.<locals>._iterencode_list(lst, _current_indent_level)
    323         else:
    324             chunks = _iterencode(value, _current_indent_level)
--> 325         yield from chunks
    326 if newline_indent is not None:
    327     _current_indent_level -= 1

File [~/miniconda3/lib/python3.9/json/encoder.py:405](https://file+.vscode-resource.vscode-cdn.net/Users/miniconda3/lib/python3.9/json/encoder.py:405), in _make_iterencode.<locals>._iterencode_dict(dct, _current_indent_level)
    403         else:
    404             chunks = _iterencode(value, _current_indent_level)
--> 405         yield from chunks
    406 if newline_indent is not None:
    407     _current_indent_level -= 1

File [~/miniconda3/lib/python3.9/json/encoder.py:405](https://file+.vscode-resource.vscode-cdn.net/Users/miniconda3/lib/python3.9/json/encoder.py:405), in _make_iterencode.<locals>._iterencode_dict(dct, _current_indent_level)
    403         else:
    404             chunks = _iterencode(value, _current_indent_level)
--> 405         yield from chunks
    406 if newline_indent is not None:
    407     _current_indent_level -= 1

    [... skipping similar frames: _make_iterencode.<locals>._iterencode_dict at line 405 (1 times)]

File [~/miniconda3/lib/python3.9/json/encoder.py:405](https://file+.vscode-resource.vscode-cdn.net/Users/miniconda3/lib/python3.9/json/encoder.py:405), in _make_iterencode.<locals>._iterencode_dict(dct, _current_indent_level)
    403         else:
    404             chunks = _iterencode(value, _current_indent_level)
--> 405         yield from chunks
    406 if newline_indent is not None:
    407     _current_indent_level -= 1

File [~/miniconda3/lib/python3.9/json/encoder.py:439](https://file+.vscode-resource.vscode-cdn.net/Users/miniconda3/lib/python3.9/json/encoder.py:439), in _make_iterencode.<locals>._iterencode(o, _current_indent_level)
    437     markers[markerid] = o
    438 o = _default(o)
--> 439 yield from _iterencode(o, _current_indent_level)
    440 if markers is not None:
    441     del markers[markerid]

File [~/miniconda3/lib/python3.9/json/encoder.py:431](https://file+.vscode-resource.vscode-cdn.net/Users/miniconda3/lib/python3.9/json/encoder.py:431), in _make_iterencode.<locals>._iterencode(o, _current_indent_level)
    429     yield from _iterencode_list(o, _current_indent_level)
    430 elif isinstance(o, dict):
--> 431     yield from _iterencode_dict(o, _current_indent_level)
    432 else:
    433     if markers is not None:

File [~/miniconda3/lib/python3.9/json/encoder.py:405](https://file+.vscode-resource.vscode-cdn.net/Users/miniconda3/lib/python3.9/json/encoder.py:405), in _make_iterencode.<locals>._iterencode_dict(dct, _current_indent_level)
    403         else:
    404             chunks = _iterencode(value, _current_indent_level)
--> 405         yield from chunks
    406 if newline_indent is not None:
    407     _current_indent_level -= 1

File [~/miniconda3/lib/python3.9/json/encoder.py:405](https://file+.vscode-resource.vscode-cdn.net/Users/miniconda3/lib/python3.9/json/encoder.py:405), in _make_iterencode.<locals>._iterencode_dict(dct, _current_indent_level)
    403         else:
    404             chunks = _iterencode(value, _current_indent_level)
--> 405         yield from chunks
    406 if newline_indent is not None:
    407     _current_indent_level -= 1

File [~/miniconda3/lib/python3.9/json/encoder.py:438](https://file+.vscode-resource.vscode-cdn.net/Users/miniconda3/lib/python3.9/json/encoder.py:438), in _make_iterencode.<locals>._iterencode(o, _current_indent_level)
    436         raise ValueError("Circular reference detected")
    437     markers[markerid] = o
--> 438 o = _default(o)
    439 yield from _iterencode(o, _current_indent_level)
    440 if markers is not None:

File [~/miniconda3/lib/python3.9/site-packages/evidently/utils/numpy_encoder.py:45](https://file+.vscode-resource.vscode-cdn.net/Users/miniconda3/lib/python3.9/site-packages/evidently/utils/numpy_encoder.py:45), in default(self, o)
      8 from evidently.core import ColumnType
      9 from evidently.utils.types import ApproxValue
     11 _TYPES_MAPPING = (
     12     (
     13         (
     14             np.int_,
     15             np.intc,
     16             np.intp,
     17             np.int8,
     18             np.int16,
     19             np.int32,
     20             np.int64,
     21             np.uint8,
     22             np.uint16,
     23             np.uint32,
     24             np.uint64,
     25         ),
     26         int,
     27     ),
     28     (
     29         (np.float_, np.float16, np.float32, np.float64),
     30         lambda obj: None if obj is np.nan else float(obj),
     31     ),
     32     ((np.ndarray,), lambda obj: obj.tolist()),
     33     ((np.bool_), bool),
     34     ((pd.Timedelta,), str),
     35     (
     36         (np.void, type(pd.NaT)),
     37         lambda obj: None,
     38     ),  # should be before datetime as NaT is subclass of datetime.
     39     ((pd.Timestamp, datetime.datetime, datetime.date), lambda obj: obj.isoformat()),
     40     # map ApproxValue to json value
     41     ((ApproxValue,), lambda obj: obj.dict()),
     42     ((pd.Series, pd.Index, pd.Categorical), lambda obj: obj.tolist()),
     43     ((pd.DataFrame,), lambda obj: obj.to_dict()),
     44     ((frozenset,), lambda obj: list(obj)),
---> 45     ((uuid.UUID,), lambda obj: str(obj)),
     46     ((ColumnType,), lambda obj: obj.value),
     47 )
     50 class NumpyEncoder(json.JSONEncoder):
     51     """Numpy and Pandas data types to JSON types encoder"""

File [~/miniconda3/lib/python3.9/json/encoder.py:179](https://file+.vscode-resource.vscode-cdn.net/Users/miniconda3/lib/python3.9/json/encoder.py:179), in JSONEncoder.default(self, o)
    160 def default(self, o):
    161     """Implement this method in a subclass such that it returns
    162     a serializable object for ``o``, or calls the base implementation
    163     (to raise a ``TypeError``).
   (...)
    177 
    178     """
--> 179     raise TypeError(f'Object of type {o.__class__.__name__} '
    180                     f'is not JSON serializable')

TypeError: Object of type Period is not JSON serializable
```

Hey @joshcx! Thanks for bug report! Please check the branch above, it should fix it

Seems to work on my end. Thanks for the quick fix @mike0sv!