diff --git a/CHANGELOG.md b/CHANGELOG.md index b96df3dc..f92e7c8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ ## Unreleased +### Fixed +- Fixed `TypeError: Type is not JSON serializable: Timestamp` when a figure contains datetime-like values such as a `pandas` `Timestamp`; these now serialize to ISO strings [[#458](https://github.com/plotly/Kaleido/issues/458)] + ## v1.3.0 ### Added diff --git a/src/py/kaleido/_kaleido_tab/_tab.py b/src/py/kaleido/_kaleido_tab/_tab.py index 3837d76a..800676ea 100644 --- a/src/py/kaleido/_kaleido_tab/_tab.py +++ b/src/py/kaleido/_kaleido_tab/_tab.py @@ -30,6 +30,8 @@ def _orjson_default(obj): """Fallback for types orjson can't handle natively (e.g. NumPy string arrays).""" if isinstance(obj, Decimal): return float(obj) + if hasattr(obj, "isoformat"): # datetime-like, e.g. pandas Timestamp (#458) + return obj.isoformat() if hasattr(obj, "tolist"): return obj.tolist() raise TypeError(f"Type is not JSON serializable: {type(obj).__name__}") diff --git a/src/py/tests/test_orjson_encoder.py b/src/py/tests/test_orjson_encoder.py new file mode 100644 index 00000000..fa3ae4d5 --- /dev/null +++ b/src/py/tests/test_orjson_encoder.py @@ -0,0 +1,31 @@ +import datetime +from decimal import Decimal + +import numpy as np +import orjson +import pandas as pd + +from kaleido._kaleido_tab._tab import _orjson_default + + +def test_orjson_default_handles_datetime_like(): + # A pandas Timestamp has no ``.tolist()`` and used to raise TypeError (#458); + # _orjson_default should fall back to an ISO string, like Plotly's encoder. + ts = pd.Timestamp("2026-06-03 12:00:00") + assert _orjson_default(ts) == ts.isoformat() + + a_date = datetime.date(2026, 1, 2) + assert _orjson_default(a_date) == a_date.isoformat() + + # A figure spec carrying a Timestamp now round-trips through orjson. + spec = {"x": [ts]} + dumped = orjson.dumps( + spec, default=_orjson_default, option=orjson.OPT_SERIALIZE_NUMPY + ) + assert ts.isoformat().encode() in dumped + + # Existing fallbacks are unaffected. + decimal_value = Decimal("1.5") + assert _orjson_default(decimal_value) == float(decimal_value) + array_values = [1, 2, 3] + assert _orjson_default(np.array(array_values)) == array_values