Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions packages/orm/src/client/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,16 @@ export type ClientOptions<Schema extends SchemaDef> = QueryOptions<Schema> & {
*/
validateInput?: boolean;

/**
* Whether to require `Date` objects (rather than ISO strings) for `DateTime` field inputs. Defaults
* to `false`, matching Prisma's longstanding behavior of coercing ISO strings — including bare
* time-only strings like `"09:00:00"` for `@db.Time` fields — to `Date`.
*
* Set to `true` to opt into strict input validation that rejects all string forms.
* @see https://github.com/zenstackhq/zenstack/issues/2631
*/
strictDateInput?: boolean;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Hey @erwan-joly , thanks for making continuous improvements. Date time handling is nasty ...

I'm thinking the new behavior (more accommodating) is probably preferred for most. Maybe we can drop this config altogether?

Copy link
Copy Markdown
Contributor Author

@erwan-joly erwan-joly May 8, 2026

Choose a reason for hiding this comment

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

Sure thing, didn’t want to “break” it if it was on purpose I see now it wasn’t so will drop the configuration altogether


/**
* Whether to use compact alias names (e.g., "$$t1", "$$t2") when transforming ORM queries to SQL.
* Defaults to `true`.
Expand Down
27 changes: 26 additions & 1 deletion packages/orm/src/client/zod/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,29 @@ export function createQuerySchemaFactory(clientOrSchema: any, options?: any) {
return new ZodSchemaFactory(clientOrSchema, options);
}

/**
* Builds a `DateTime` value schema that accepts a `Date` object or an ISO
* datetime / date / time-only string and coerces it to a `Date`. Time-only
* strings (e.g. `"09:00:00"` for `@db.Time` fields) are anchored to the Unix
* epoch. Strings that don't parse fall through and are rejected by `z.date()`
* with the standard error.
*
* Used when `ClientOptions.strictDateInput` is left at its default (`false`).
* @see https://github.com/zenstackhq/zenstack/issues/2631
*/
export function coercedDateTimeSchema(): ZodType {
return z.preprocess((val) => {
if (typeof val !== 'string') return val;
if (/^\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d\d(?::\d\d)?)?$/.test(val)) {
const hasTz = val.endsWith('Z') || /[+-]\d\d(?::\d\d)?$/.test(val);
const d = new Date(`1970-01-01T${val}${hasTz ? '' : 'Z'}`);
return isNaN(d.getTime()) ? val : d;
}
const d = new Date(val);
return isNaN(d.getTime()) ? val : d;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}, z.date());
}

/**
* Options for creating Zod schemas.
*/
Expand Down Expand Up @@ -854,7 +877,9 @@ export class ZodSchemaFactory<

@cache()
private makeDateTimeValueSchema(): ZodType {
const schema = z.union([z.iso.datetime(), z.iso.date(), z.date()]);
const schema = (this.options as ClientOptions<Schema>)?.strictDateInput
? z.union([z.iso.datetime(), z.iso.date(), z.date()])
: coercedDateTimeSchema();
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
this.registerSchema('DateTime', schema);
return schema;
}
Expand Down
11 changes: 10 additions & 1 deletion packages/zod/src/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,16 @@ class SchemaFactory<Schema extends SchemaDef> {
]);
break;
case 'DateTime':
base = z.union([z.date(), z.iso.datetime()]);
base = z.preprocess((val) => {
if (typeof val !== 'string') return val;
if (/^\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d\d(?::\d\d)?)?$/.test(val)) {
const hasTz = val.endsWith('Z') || /[+-]\d\d(?::\d\d)?$/.test(val);
const d = new Date(`1970-01-01T${val}${hasTz ? '' : 'Z'}`);
return isNaN(d.getTime()) ? val : d;
}
const d = new Date(val);
return isNaN(d.getTime()) ? val : d;
}, z.date());
break;
case 'Bytes':
base = z.instanceof(Uint8Array);
Expand Down
47 changes: 47 additions & 0 deletions packages/zod/test/factory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,53 @@ describe('SchemaFactory - makeModelSchema', () => {
birthdate: '2024-01-15T10:30:00.000Z',
});
expect(result.success).toBe(true);
// Coerced to a Date for the engine.
expect(result.data?.birthdate).toBeInstanceOf(Date);
});

// Regression: #2631 — earlier versions accepted ISO strings via
// Prisma's permissive coercion. The strict zod union introduced in
// 3.5+ rejected ISO date and time-only strings, breaking every
// `@db.Date` and `@db.Time` caller that had been passing strings.
it('accepts DateTime as an ISO date string (#2631)', () => {
const userSchema = factory.makeModelSchema('User');
const result = userSchema.safeParse({
...validUser,
birthdate: '2024-01-15',
});
expect(result.success).toBe(true);
expect(result.data?.birthdate).toBeInstanceOf(Date);
});

it('accepts DateTime as a bare time-only string for @db.Time fields (#2631)', () => {
const userSchema = factory.makeModelSchema('User');
const result = userSchema.safeParse({
...validUser,
birthdate: '09:30:00',
});
expect(result.success).toBe(true);
// Time-only strings are anchored to the Unix epoch.
expect(result.data?.birthdate).toBeInstanceOf(Date);
expect((result.data?.birthdate as Date).toISOString()).toBe('1970-01-01T09:30:00.000Z');
});

it('accepts DateTime as a time-only string with timezone (#2631)', () => {
const userSchema = factory.makeModelSchema('User');
const result = userSchema.safeParse({
...validUser,
birthdate: '09:30:00+12:00',
});
expect(result.success).toBe(true);
expect(result.data?.birthdate).toBeInstanceOf(Date);
});

it('rejects DateTime as a non-parseable string', () => {
const userSchema = factory.makeModelSchema('User');
const result = userSchema.safeParse({
...validUser,
birthdate: 'not-a-date',
});
expect(result.success).toBe(false);
});

it('accepts Bytes as Uint8Array', () => {
Expand Down
Loading