Skip to content

fix(orm): coerce ISO strings on DateTime input, with strictDateInput opt-in (#2631)#2632

Open
erwan-joly wants to merge 4 commits intozenstackhq:devfrom
erwan-joly:fix/2631-coerce-iso-strings-on-datetime-input
Open

fix(orm): coerce ISO strings on DateTime input, with strictDateInput opt-in (#2631)#2632
erwan-joly wants to merge 4 commits intozenstackhq:devfrom
erwan-joly:fix/2631-coerce-iso-strings-on-datetime-input

Conversation

@erwan-joly
Copy link
Copy Markdown
Contributor

@erwan-joly erwan-joly commented Apr 30, 2026

Fixes #2631.

Background

The strict zod input validator introduced in 3.5+ broke every caller passing ISO strings to DateTime fields, including bare time-only strings like "09:00:00" for @db.Time columns. Earlier versions silently coerced these via Prisma's input layer; the new validator rejects them outright. Existing user code that worked unchanged across years of Prisma → ZenStack v2 → ZenStack v3.4 suddenly fails with Invalid input: expected date, received string.

Approach

Per option 2 from the issue, this restores Prisma-compatible coercion as the default while leaving strict validation available behind a new opt-in flag. That keeps existing callers working while preserving the stricter semantics for users who want them:

const db = new ZenStackClient(schema, {
  dialect: ...,
  strictDateInput: true, // opt in to date-objects-only validation
});

Default (strictDateInput: false or unset) accepts:

  • Date objects (already worked)
  • ISO datetime strings, e.g. "2024-01-15T10:30:00.000Z" (already worked)
  • ISO date strings, e.g. "2024-01-15" (already worked in orm factory; new in standalone zod factory)
  • ISO time-only strings, e.g. "09:30:00", "09:30:00.123", "09:30:00Z", "09:30:00+12:00" (new)

All string forms are coerced to a Date for the engine. Time-only strings are anchored to the Unix epoch (1970-01-01T<time>), matching the existing OID-1083 read-side behavior introduced in #2590.

Files changed

  • packages/orm/src/client/options.ts — new strictDateInput?: boolean option with JSDoc explaining the default and tradeoff
  • packages/orm/src/client/zod/factory.ts — new exported helper coercedDateTimeSchema(); makeDateTimeValueSchema switches on strictDateInput
  • packages/zod/src/factory.ts — same coercion applied in the standalone factory (no setting — these schemas are typically used for form validation where coercion is more important)
  • packages/zod/test/factory.test.ts — regression tests for the four accepted forms plus a non-parseable rejection case, all referencing ZenStack 3.6: strict Date input validator rejects ISO strings (regression vs 3.4 / Prisma) #2631

Tests

✓ accepts DateTime as a Date object
✓ accepts DateTime as an ISO datetime string
✓ accepts DateTime as an ISO date string (#2631)
✓ accepts DateTime as a bare time-only string for @db.Time fields (#2631)
✓ accepts DateTime as a time-only string with timezone (#2631)
✓ rejects DateTime as a non-parseable string

The strict path (when strictDateInput: true) keeps the existing z.union([z.iso.datetime(), z.iso.date(), z.date()]) behavior unchanged, so users who've adopted the strict semantics on 3.5/3.6 are unaffected.

Happy to adjust naming (strictDateInput vs strictDateInputs vs anything else) or re-shape per review.

Summary by CodeRabbit

  • New Features

    • Added a client option, strictDateInput — when enabled, DateTime fields require Date objects; when disabled (default), ISO datetime/date strings and time-only strings are coerced to Date for backward compatibility.
  • Tests

    • Added regression tests covering default and strict validation modes, including time-only string coercion and rejection cases.

…input (zenstackhq#2631)

The strict zod union introduced in 3.5+ broke every caller passing ISO
strings to `DateTime` fields, including bare time-only strings like
"09:00:00" for `@db.Time` columns. Earlier versions coerced these via
Prisma's input layer; the new validator rejected them outright with
no migration path called out in the release notes.

This restores Prisma-compatible coercion as the default while leaving
strict validation available behind a new `ClientOptions.strictDateInput`
flag (default `false`) for users who want to opt in.

Changes:
  - `packages/orm/src/client/options.ts`: new `strictDateInput?: boolean`
  - `packages/orm/src/client/zod/factory.ts`: new exported helper
    `coercedDateTimeSchema()` that anchors time-only strings to the Unix
    epoch and falls through to `z.date()` for all other paths;
    `makeDateTimeValueSchema` switches on `strictDateInput`
  - `packages/zod/src/factory.ts`: same coercion applied in the
    standalone factory (no setting — these schemas are typically used
    for form validation where coercion is even more important)
  - `packages/zod/test/factory.test.ts`: regression tests for the four
    accepted forms (Date, ISO datetime, ISO date, time-only with and
    without timezone) plus a non-parseable rejection case

Fixes zenstackhq#2631
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 30, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 998ed761-057b-41ce-bf5a-fe953a776749

📥 Commits

Reviewing files that changed from the base of the PR and between 6956c7e and 73fe8c8.

📒 Files selected for processing (1)
  • packages/orm/src/client/zod/factory.ts

📝 Walkthrough

Walkthrough

Adds strictDateInput to client options (default false), implements a coercing DateTime Zod schema that parses ISO datetime/date/time strings into Date when not strict, and adds regression tests for both modes.

Changes

Cohort / File(s) Summary
Configuration
packages/orm/src/client/options.ts
Added optional strictDateInput?: boolean to ClientOptions to toggle DateTime input validation (default: false).
Validation Schema
packages/orm/src/client/zod/factory.ts
Added exported coercedDateTimeSchema() to preprocess/convert ISO datetime, date-only, and time-only strings into Date; updated makeDateTimeValueSchema() to use z.date() when strictDateInput is true, otherwise use the coercing schema.
Regression Tests
tests/regression/test/issue-2631.test.ts
Added tests verifying default coercion of ISO datetime/date/time strings to Date and strict mode rejecting non-Date inputs; also checks rejection of unparseable strings.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I hop through timestamps, sniffing each string,

"Is it a Date?" I ask — sometimes I bring
an ISO, a day, or just "09:00" to play;
if you ask me to be strict, I’ll toss strings away.
🥕 Time parsed or guarded — the rabbit keeps pace.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately describes the main change: introducing ISO string coercion for DateTime input with a strict mode option.
Linked Issues check ✅ Passed The PR fully addresses issue #2631 by restoring Prisma-compatible coercion of ISO strings for DateTime inputs by default and providing strictDateInput opt-in.
Out of Scope Changes check ✅ Passed All changes are directly aligned with the linked issue requirements: adding strictDateInput option, implementing coercion schema, and adding comprehensive regression tests.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 7/8 reviews remaining, refill in 7 minutes and 30 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/zod/test/factory.test.ts (1)

161-204: ⚡ Quick win

Add strict-mode regression coverage too.

These tests validate the default coercion path well, but they don’t cover strictDateInput: true behavior in ORM schema generation. Adding that case will lock the contract and prevent drift.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/zod/test/factory.test.ts` around lines 161 - 204, Add parallel tests
that construct the model schema with strictDateInput enabled (e.g., call
factory.makeModelSchema('User', { strictDateInput: true })) and assert that ISO
date strings, bare time strings, and timezone time strings are rejected
(result.success === false), while actual Date instances still pass; add one test
per case mirroring the existing ones (names like "strict mode rejects ISO date
string", "strict mode rejects time-only string", etc.) to lock the strict-mode
contract.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/orm/src/client/zod/factory.ts`:
- Around line 880-882: The current strictDateInput branch for building schema
allows ISO strings via z.iso.datetime() and z.iso.date(), which contradicts the
ClientOptions<Schema> docs saying strict mode rejects all string forms; change
the implementation in the schema construction so that when (this.options as
ClientOptions<Schema>)?.strictDateInput is true the schema is strictly z.date()
(no z.iso.* unions) and otherwise use coercedDateTimeSchema(); update or run
related tests that assumed ISO-string acceptance if you instead choose to keep
the current behavior and prefer updating docs/tests to state that
strictDateInput still accepts ISO strings.

---

Nitpick comments:
In `@packages/zod/test/factory.test.ts`:
- Around line 161-204: Add parallel tests that construct the model schema with
strictDateInput enabled (e.g., call factory.makeModelSchema('User', {
strictDateInput: true })) and assert that ISO date strings, bare time strings,
and timezone time strings are rejected (result.success === false), while actual
Date instances still pass; add one test per case mirroring the existing ones
(names like "strict mode rejects ISO date string", "strict mode rejects
time-only string", etc.) to lock the strict-mode contract.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9373f55b-22da-4966-9cc6-7778b82071d2

📥 Commits

Reviewing files that changed from the base of the PR and between a31a32e and 08d8a8d.

📒 Files selected for processing (4)
  • packages/orm/src/client/options.ts
  • packages/orm/src/client/zod/factory.ts
  • packages/zod/src/factory.ts
  • packages/zod/test/factory.test.ts

Comment thread packages/orm/src/client/zod/factory.ts Outdated
- Strict mode now actually rejects all string forms (was still accepting
  ISO datetime/date because the union still included z.iso.datetime() /
  z.iso.date()). Switched to plain z.date() so the implementation
  matches the JSDoc on ClientOptions.strictDateInput.

- Reverted the standalone @zenstackhq/zod factory back to its previous
  union (z.date() | z.iso.datetime()). The OpenAPI generator emits its
  spec from this factory and the preprocess version produced a
  different schema shape that broke the rpc-openapi baseline test. The
  ORM-side coercion in @zenstackhq/orm/zod/factory.ts still covers the
  runtime input validation that zenstackhq#2631 was about; standalone consumers
  can layer their own coercion if they need it.

- Replaced the standalone-factory regression tests with an end-to-end
  ORM regression suite at tests/regression/test/issue-2631.test.ts
  covering both default coercion (Date, ISO datetime, ISO date,
  time-only with and without TZ) and the strict-mode contract (Date
  only — ISO datetime, ISO date, and time-only strings all rejected).
@erwan-joly
Copy link
Copy Markdown
Contributor Author

Thanks for the catch — pushed e8cc785e addressing both points:

  1. Strict mode now actually strict — switched the strict branch from z.union([z.iso.datetime(), z.iso.date(), z.date()]) to plain z.date() so it matches the JSDoc.

  2. Standalone factory reverted — that was breaking the rpc-openapi baseline test because the z.preprocess shape differs from the prior union. Reverted to z.union([z.date(), z.iso.datetime()]) for that path. The ORM-side coercion in packages/orm/src/client/zod/factory.ts (the runtime validator users hit) keeps the fix; standalone consumers who want coercion can layer it themselves.

  3. Strict-mode tests added — new tests/regression/test/issue-2631.test.ts covers the default behavior end-to-end (Date, ISO datetime, ISO date, time-only with and without TZ, non-parseable rejection) and the strict contract (Date-only, all string forms rejected).

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
tests/regression/test/issue-2631.test.ts (1)

18-77: ⚡ Quick win

Add explicit coverage for strictDateInput: false and TZ/fraction time-only variants

Coverage is good overall, but this suite doesn’t currently assert the explicit strictDateInput: false path, nor the documented time-only variants (09:30:00.123, 09:30:00Z, 09:30:00+12:00).

🧪 Suggested additions
         it('accepts a bare time-only string anchored to the Unix epoch', async () => {
             const e = await db.event.create({ data: { label: 'time-only', when: '09:30:00' } });
             expect(e.when).toBeInstanceOf(Date);
             expect((e.when as Date).getUTCHours()).toBe(9);
             expect((e.when as Date).getUTCMinutes()).toBe(30);
         });
+
+        it.each(['09:30:00.123', '09:30:00Z', '09:30:00+12:00'])(
+            'accepts time-only variant %s',
+            async (when) => {
+                const e = await db.event.create({ data: { label: `time-${when}`, when } });
+                expect(e.when).toBeInstanceOf(Date);
+            }
+        );
+
+        it('accepts ISO strings when strictDateInput is explicitly false', async () => {
+            const laxDb = await createTestClient(schema, {
+                usePrismaPush: true,
+                provider: 'sqlite',
+                strictDateInput: false,
+            });
+            try {
+                await expect(
+                    laxDb.event.create({ data: { label: 'explicit-false', when: '2024-01-15T10:30:00.000Z' } })
+                ).resolves.toBeTruthy();
+            } finally {
+                await laxDb.$disconnect();
+            }
+        });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/regression/test/issue-2631.test.ts` around lines 18 - 77, Tests are
missing an explicit describe for strictDateInput: false and additional time-only
string variants; add a new describe or expand the existing "default
(strictDateInput unset / false)" block to explicitly create the client with
strictDateInput: false via createTestClient(schema, { usePrismaPush: true,
provider: 'sqlite', strictDateInput: false }) and add itests that assert
time-only strings with fractions and timezones ('09:30:00.123', '09:30:00Z',
'09:30:00+12:00') are accepted and coerced to Date (and preserve expected UTC
hour/minute), while keeping the existing non-parseable-string rejection test;
reference the db.event.create calls and the expect(e.when).toBeInstanceOf(Date)
/ getUTCHours()/getUTCMinutes() assertions to guide placement.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/orm/src/client/zod/factory.ts`:
- Around line 105-106: The fallback date parsing currently uses new Date(val)
(variables d and val in packages/orm/src/client/zod/factory.ts) and therefore
accepts non-ISO engine-dependent formats; change it to first validate that val
matches the allowed ISO patterns (e.g., ISO date, ISO datetime with optional
timezone, or time-only formats documented) using an explicit regex or parser
check, and only then construct new Date(val) and return the Date; if the ISO
validation fails, return the original val unchanged to preserve the documented
ISO-only contract.

---

Nitpick comments:
In `@tests/regression/test/issue-2631.test.ts`:
- Around line 18-77: Tests are missing an explicit describe for strictDateInput:
false and additional time-only string variants; add a new describe or expand the
existing "default (strictDateInput unset / false)" block to explicitly create
the client with strictDateInput: false via createTestClient(schema, {
usePrismaPush: true, provider: 'sqlite', strictDateInput: false }) and add
itests that assert time-only strings with fractions and timezones
('09:30:00.123', '09:30:00Z', '09:30:00+12:00') are accepted and coerced to Date
(and preserve expected UTC hour/minute), while keeping the existing
non-parseable-string rejection test; reference the db.event.create calls and the
expect(e.when).toBeInstanceOf(Date) / getUTCHours()/getUTCMinutes() assertions
to guide placement.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 50f30409-6a8e-4ba1-975d-65bff2e41745

📥 Commits

Reviewing files that changed from the base of the PR and between 08d8a8d and e8cc785.

📒 Files selected for processing (2)
  • packages/orm/src/client/zod/factory.ts
  • tests/regression/test/issue-2631.test.ts

Comment thread packages/orm/src/client/zod/factory.ts
erwan-joly added 2 commits May 1, 2026 00:35
Addresses CodeRabbit nitpick on zenstackhq#2632: the implementation falls through
to `new Date(val)` for non-time-only strings, so engine-dependent
formats like "2024/01/15" are accepted. That is intentional — the
schema mirrors Prisma's pre-3.5 behaviour for compatibility — but the
JSDoc previously said "ISO strings" only. Reword to describe the actual
contract and point users who want stricter validation at
strictDateInput.
`coercedDateTimeSchema` previously returned `z.preprocess(fn, z.date())`,
which serialised to an empty `{}` and broke the rpc-openapi baseline
(`packages/server/test/openapi/baseline/rpc.baseline.yaml`) that
documents the accepted ISO datetime / ISO date / Date forms. Wrap the
preprocess around the original `z.union([z.iso.datetime(), z.iso.date(),
z.date()])` so OpenAPI generation still emits the documented variants.
Runtime behaviour is unchanged: preprocess coerces strings into Dates
first, the union's `z.date()` arm catches everything that parses, and
non-parseable strings fall through and are rejected.
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.

ZenStack 3.6: strict Date input validator rejects ISO strings (regression vs 3.4 / Prisma)

1 participant