Skip to content

Serialize datetime-like values in _orjson_default - #461

Merged
camdecoster merged 4 commits into
plotly:mainfrom
binggao1230:fix-458-datetime-serialization
Jul 21, 2026
Merged

camdecoster merged 4 commits into
plotly:mainfrom
binggao1230:fix-458-datetime-serialization

Conversation

@binggao1230

@binggao1230 binggao1230 commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Summary

Exporting a figure that contains a datetime-like value — e.g. a pandas Timestamp used as a marker position — raises:

TypeError: Type is not JSON serializable: Timestamp

(reported in #458, a regression in 1.3.0). The orjson fallback _orjson_default only handles Decimal and objects exposing .tolist() (NumPy). A Timestamp has neither, so it falls through to raise TypeError.

Fix

Serialize datetime-like objects — anything with .isoformat() (pandas Timestamp, datetime, date) — to ISO strings, which is how Plotly's own JSON encoder represents them. The check is placed before the .tolist() branch so numpy.datetime64 (which has .tolist() but no .isoformat()) is unaffected.

Closes #458.

Tests

Added a unit test for _orjson_default covering a Timestamp, a date, an end-to-end orjson.dumps of a spec carrying a Timestamp, and the existing Decimal/NumPy fallbacks. It fails on master with the original TypeError and passes with the fix.

A figure containing a pandas Timestamp (e.g. a marker x-position) raised
`TypeError: Type is not JSON serializable: Timestamp` from write_image /
calc_fig. The orjson fallback `_orjson_default` only handled `Decimal`
and objects exposing `.tolist()` (NumPy); a Timestamp has neither, so it
fell through to `raise TypeError`.

Serialize datetime-like objects (anything with `.isoformat()`, e.g.
pandas `Timestamp`, `datetime`, `date`) to ISO strings, matching how
Plotly's own JSON encoder handles them.
@awiol

awiol commented Jul 7, 2026

Copy link
Copy Markdown

Related to #465
Although, I have to note that problem a better solution would be to bring back cleaning done by Plotly, instead of duplicating logic that already is implemented in Plotly into kaleido, as new regressions are discovered.

@camdecoster camdecoster changed the title Serialize datetime-like values in _orjson_default (fixes #458) Serialize datetime-like values in _orjson_default Jul 21, 2026
@camdecoster
camdecoster merged commit 27219ad into plotly:main Jul 21, 2026
4 checks passed
@camdecoster

Copy link
Copy Markdown
Contributor

Thanks for the PR! This is in an incremental fix to unblock some users, but the rest of the underlying issue in #465 still needs to be dealt with.

egordm pushed a commit to OpenSTEF/openstef that referenced this pull request Sep 21, 2026
## What does this PR do?

Lifts the kaleido pin in `docs/` and `examples/` from the deprecated
0.2.x line to `>=1.4`, and refreshes `uv.lock`.

Closes #938

The issue's acceptance criteria offered two routes: fix the plotter, or
"confirm kaleido >1.3.x fixed the regression upstream". The second one
is now met.

@egordm's analysis pinned the bug to `_orjson_default` in
`kaleido/_kaleido_tab/_tab.py`, which only handled `.tolist()` and
raised on `pd.Timestamp`, and noted that no upstream PR existed yet. One
does now: plotly/Kaleido#461 by @gaoflow adds an `isoformat()` fallback
for datetime-like objects, shipped in kaleido v1.4.0 on 2026-08-31. The
fix itself is theirs; what this PR adds is the verification and the
lift.

Because it sits in the central serializer rather than at a call site, it
covers the paths the plotter-side workaround could not reach, which is
why #942 reverted `_normalize_x_data` and constrained the dependency
instead.

### Verification

**1. The call sites, 1.3.0 versus 1.4.0.** Same script, same plotly
7.1.0 and pandas 3.0.5, only kaleido differs:

| Figure content | kaleido 1.3.0 | kaleido 1.4.0 |
| --- | --- | --- |
| quantile-fill band (the repro in #938) | `TypeError: Type is not JSON
serializable: Timestamp` | exports |
| `add_vrect` with Timestamp bounds | same `TypeError` | exports |
| `layout.xaxis.range` with Timestamps | same `TypeError` | exports |
| annotation anchored at a Timestamp | same `TypeError` | exports |

The last three are the call sites #938 lists as not covered by the
workaround.

**2. The real plotter.** `ForecastTimeSeriesPlotter` with measurements,
a forecast and P10/P50/P90 bands, exported through `pio.to_image`,
succeeds on 1.4.0 (60054 bytes). It also succeeds on 1.3.0, because
`_add_single_quantile_polygon` already keeps `x` as a `DatetimeIndex`.
That workaround is left untouched: it costs nothing and keeps the export
robust against future regressions, as the issue describes.

**3. The docs build.** `docs-check` run on my fork against this branch:
`uv sync --frozen` then `poe docs`, green on ubuntu-latest, 24.7 MB of
HTML uploaded. `nb_execution_mode` is `cache` and a fresh runner has no
cache, so the tutorials were executed and their figures exported through
kaleido 1.4.0 rather than replayed.

### Why `>=1.4` and not `>=1.0`

1.0 through 1.3 still carry the Timestamp bug, so the floor has to be
the release containing plotly/Kaleido#461.

This also resolves the other two reasons the pin mentioned: v1 is the
maintained line, so the deprecated v0 goes away, and 1.4.0 ships a
single `py3-none-any` wheel, so the missing macOS arm64 wheel that ruled
out `0.2.1.post1` is no longer a factor. Note that v1 does not bundle a
browser and uses a system Chrome or Chromium, which the CI runner
already has, as the green docs build shows.

## Type of change

- [ ] Bug fix
- [ ] New feature
- [ ] Breaking change (see checklist below)
- [ ] Documentation
- [x] Refactor / chore / CI

## Breaking changes checklist

- [ ] Public API, config schema, or serialized/pickled objects changed
in a way that affects existing users

Not applicable: this only moves a docs/tutorial build dependency. No
library code changes.

## AI disclosure

- [ ] No AI assistance was used (beyond grammar/spelling)
- [x] AI assistance was used — tool(s): Claude Opus 5, as a research and
drafting aid under my direction
- [x] I have reviewed, understand, and can explain all AI-generated code
in this PR
- [x] This is disclosed in a commit message (`Assisted-by: Claude Opus
5`)

## Checklist

- [ ] `poe all --check` passes locally
- [ ] Tests added/updated for the change
- [ ] Documentation updated (docstrings, user guide, examples) if needed
- [x] Commits are signed off per our DCO (`git commit -s`)
- [x] PR title follows Conventional Commits

On the two unchecked boxes: there is no code change to test, and the
upstream behaviour is already covered by `test_orjson_encoder.py` in
plotly/Kaleido#461. I ran the docs build rather than `poe all --check`,
since that is the job this pin affects; happy to run the full check if
you would like it on the record.

---------

Signed-off-by: Jakub Krasuski <kubamax4@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Regression in kaleido 1.3.0 when saving plot with markers

3 participants