Skip to content

fix(core): accept naive datetime-local values for datetime fields so they round-trip (#1368)#1561

Open
marcusbellamyshaw-cell wants to merge 2 commits into
emdash-cms:mainfrom
Emdash-Bug-Testing:fix/1368-datetime-accept-naive
Open

fix(core): accept naive datetime-local values for datetime fields so they round-trip (#1368)#1561
marcusbellamyshaw-cell wants to merge 2 commits into
emdash-cms:mainfrom
Emdash-Bug-Testing:fix/1368-datetime-accept-naive

Conversation

@marcusbellamyshaw-cell

Copy link
Copy Markdown
Contributor

What does this PR do?

A datetime field could not be saved through its own admin editor. The generated validator was z.string().datetime().or(z.string().date()), which only accepts ISO with a Z suffix or a bare YYYY-MM-DD date. But <input type="datetime-local"> (and many seeds) produce a naive datetime such as 2026-06-04T18:30:00 (no Z, no offset). Because the admin re-sends every loaded field on autosave, a stored naive datetime failed validation and the entry became unsavable — the same class of round-trip bug as #867.

The validator now uses z.iso.datetime({ offset: true, local: true }).or(z.iso.date()), which accepts ISO with Z, ISO with a timezone offset, naive datetimes (with or without seconds), and date-only values, while still rejecting non-dates and impossible dates. This also replaces the deprecated z.string().datetime() API.

Closes #1368

Type of change

  • Bug fix

Checklist

  • I have read CONTRIBUTING.md
  • pnpm typecheck passes
  • pnpm lint passes
  • pnpm test passes (targeted: tests/unit/schema — 115 tests)
  • pnpm format has been run
  • I have added/updated tests for my changes
  • I have added a changeset

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: Claude Opus 4.8 ultracode

…they round-trip (emdash-cms#1368)

A datetime field could not be saved through its own admin editor. The generated
validator was z.string().datetime().or(z.string().date()), which only accepts
ISO with a Z suffix or a bare YYYY-MM-DD date. But <input type="datetime-local">
(and many seeds) produce a naive datetime such as 2026-06-04T18:30:00 (no Z, no
offset). Because the admin re-sends every loaded field on autosave, a stored
naive datetime failed validation and the entry became unsavable -- the same
class of round-trip bug as emdash-cms#867.

The validator now uses z.iso.datetime({ offset: true, local: true }).or(z.iso.date()),
which accepts ISO with Z, ISO with a timezone offset, naive datetimes (with or
without seconds), and date-only values, while still rejecting non-dates and
impossible dates. This also replaces the deprecated z.string().datetime() API.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Jun 20, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 8ed7b4f

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 16 packages
Name Type
emdash Patch
@emdash-cms/cloudflare Patch
@emdash-cms/sandbox-workerd Patch
@emdash-cms/fixture-perf-site Patch
@emdash-cms/perf-demo-site Patch
@emdash-cms/cache-demo-site Patch
@emdash-cms/do-demo-site Patch
@emdash-cms/do-solo-demo-site Patch
@emdash-cms/admin Patch
@emdash-cms/auth Patch
@emdash-cms/blocks Patch
@emdash-cms/gutenberg-to-portable-text Patch
@emdash-cms/x402 Patch
create-emdash Patch
@emdash-cms/auth-atproto Patch
@emdash-cms/plugin-embeds Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approach

This is the right change for the right problem. Issue #1368 is a genuine round-trip bug: a datetime field value produced by <input type="datetime-local"> or a seed (a naive 2026-06-04T18:30:00, no Z/offset) was rejected by the generated validator z.string().datetime().or(z.string().date()), and because the admin re-sends every loaded field on autosave, the entry became unsavable through its own editor. The fix mirrors the established #867 pattern (broaden the round-trip validator while keeping semantic validation) and is strictly additive — the new accepted set is a superset of the old one, so there's no backwards-compat regression. Good taste, well scoped (no drive-by cleanup of the other deprecated z.string().datetime() call sites, which is correct per the scope-discipline rule).

What I checked

  • Placement: The validator lives in getBaseSchema (zod-generator.ts), which feeds generateZodSchemavalidateContentData (api/handlers/validation.ts), the exact path content create/update (and thus admin autosave) validates through. The fix reaches the round-trip path. ✓
  • Behavior: z.iso.datetime({ offset: true, local: true }).or(z.iso.date()) accepts Z, offset, naive (with/without seconds), and date-only values, and rejects garbage / impossible dates. z.iso.* is the correct Zod 4 API (catalog pins zod@^4.4.1); the deprecated z.string().datetime() is correctly replaced. ✓
  • Serialization: serializeValue/deserializeValue store datetimes as TEXT and return them as strings (no JSON reparse, no Date coercion), so a stored naive datetime round-trips as the same string the validator now accepts. ✓
  • Validation interactions: applyValidation only applies string/number refinements when schema instanceof z.ZodString/ZodNumber; the datetime schema is a union in both old and new code, so pattern/minLength were already no-ops — no behavior change. The required-empty-string check in validateContentData is independent and unaffected. ✓
  • Test quality: The new test exercises the real generateFieldSchema (no mocking), and its "accepts naive datetime" cases would fail against the old validator, so it's a true reproducer, not false confidence. The Field helper omits searchable/translatable, but tests are excluded from packages/core typecheck and generateFieldSchema never reads them — consistent with the existing zod-generator.test.ts. ✓
  • Sibling paths: contentDateOverride / contentDateBound (for publishedAt/createdAt overrides and the list date-range filter) still require an offset and don't accept naive values. These are pre-existing, serve a different intent (precise timestamp overrides vs. round-trippable field values), and aren't touched by this PR — not a regression, and correctly left alone.

I could not execute the suite to confirm Zod 4.4.1's exact regex accepts the no-seconds form (2024-01-15T14:30), but the API and the test design are correct, and the no-seconds case is optional in Zod 4's ISO time portion.

Headline

The code and test are clean and correct. The only finding is the changeset prose, which reads like a commit message / PR description rather than a user-facing release note — a violation of the explicitly documented changeset convention in CONTRIBUTING.md.

Comment on lines +5 to +9
fix(core): accept naive datetime-local values for datetime fields so they round-trip (#1368)

A `datetime` field could not be saved through its own admin editor. The generated validator was `z.string().datetime().or(z.string().date())`, which only accepts ISO with a `Z` suffix or a bare `YYYY-MM-DD` date. But `<input type="datetime-local">` (and many seeds) produce a naive datetime such as `2026-06-04T18:30:00` (no `Z`, no offset). Because the admin re-sends every loaded field on autosave, a stored naive datetime failed validation and the entry became unsavable — the same class of round-trip bug as #867.

The validator now uses `z.iso.datetime({ offset: true, local: true }).or(z.iso.date())`, which accepts ISO with `Z`, ISO with a timezone offset, naive datetimes (with or without seconds), and date-only values, while still rejecting non-dates and impossible dates. This also replaces the deprecated `z.string().datetime()` API.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[needs fixing] This changeset reads like a commit message / PR description rather than a user-facing release note, which CONTRIBUTING.md §Changesets explicitly prohibits: "It is not a commit message, a PR description, or a summary of your diff. Don't paste your PR text into it … Start with a present-tense verb … Leave out internal mechanics — file names, function names … how you implemented it."

Two concrete deviations: (1) the heading uses conventional-commit format fix(core): …, and (2) the body is the PR description pasted verbatim, naming the old/new validator implementation (z.string().datetime().or(z.string().date()), z.iso.datetime({ offset: true, local: true })). None of the existing changesets in this repo use a type(scope): heading — they all lead with a bare present-tense verb ("Fixes …", "Speeds up …", "Declare …"). Rewrite it for the reader who will run the new version:

Suggested change
fix(core): accept naive datetime-local values for datetime fields so they round-trip (#1368)
A `datetime` field could not be saved through its own admin editor. The generated validator was `z.string().datetime().or(z.string().date())`, which only accepts ISO with a `Z` suffix or a bare `YYYY-MM-DD` date. But `<input type="datetime-local">` (and many seeds) produce a naive datetime such as `2026-06-04T18:30:00` (no `Z`, no offset). Because the admin re-sends every loaded field on autosave, a stored naive datetime failed validation and the entry became unsavable — the same class of round-trip bug as #867.
The validator now uses `z.iso.datetime({ offset: true, local: true }).or(z.iso.date())`, which accepts ISO with `Z`, ISO with a timezone offset, naive datetimes (with or without seconds), and date-only values, while still rejecting non-dates and impossible dates. This also replaces the deprecated `z.string().datetime()` API.
Fixes `datetime` fields rejecting values from the admin editor's date/time picker, so entries with a datetime field can be saved and autosaved again. Values without a timezone (e.g. `2026-06-04T18:30:00`, as produced by `<input type="datetime-local">` and many seeds) and values with a timezone offset now round-trip cleanly instead of failing validation on the next save.

@github-actions github-actions Bot added review/awaiting-author Reviewed; waiting on the author to respond and removed review/needs-review No maintainer or bot review yet labels Jun 20, 2026
…elease note

Per CONTRIBUTING.md, a changeset is the release note a user reads while
upgrading, not a commit message or diff summary. Drop the commit-message
title line and internal validator mechanics; lead with the observable effect.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions github-actions Bot added review/needs-rereview Author pushed changes since the last review and removed review/awaiting-author Reviewed; waiting on the author to respond labels Jun 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/core cla: signed review/needs-rereview Author pushed changes since the last review size/M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

datetime field cannot round-trip through admin (autosave: Invalid input)

1 participant