Skip to content

Commit 6176dcb

Browse files
authored
Improve ISO second handling (#4680)
* Improve ISO second handling * Fixlint * Tweak test * Add precision -1. Remove offset normalization. Make seconds optional by default * Update docs
1 parent 112fff6 commit 6176dcb

File tree

14 files changed

+835
-708
lines changed

14 files changed

+835
-708
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"lint-staged": "^12.5.0",
3333
"mitata": "^0.1.14",
3434
"prettier": "^3.5.3",
35+
"recheck": "^4.5.0",
3536
"semver": "^7.7.2",
3637
"supershy": "^1.0.0",
3738
"tinybench": "^2.9.0",

packages/docs/content/api.mdx

Lines changed: 51 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -355,41 +355,59 @@ The `z.iso.datetime()` method enforces ISO 8601; by default, no timezone offsets
355355
```ts
356356
const datetime = z.iso.datetime();
357357

358-
datetime.parse("2020-01-01T00:00:00Z"); //
359-
datetime.parse("2020-01-01T00:00:00.123Z"); //
360-
datetime.parse("2020-01-01T00:00:00.123456Z"); // ✅ (arbitrary precision)
361-
datetime.parse("2020-01-01T00:00:00+02:00"); // ❌ (no offsets allowed)
358+
datetime.parse("2020-01-01T06:15:00Z"); //
359+
datetime.parse("2020-01-01T06:15:00.123Z"); //
360+
datetime.parse("2020-01-01T06:15:00.123456Z"); // ✅ (arbitrary precision)
361+
datetime.parse("2020-01-01T06:15:00+02:00"); // ❌ (offsets not allowed)
362+
datetime.parse("2020-01-01T06:15:00"); // ❌ (local not allowed)
362363
```
363364

364365
To allow timezone offsets:
365366

366367
```ts
367368
const datetime = z.iso.datetime({ offset: true });
368369

369-
// result is normalized to RFC 3339 format
370-
datetime.parse("2020-01-01T00:00:00+02"); // ✅ "2020-01-01T00:00:00+02:00"
371-
datetime.parse("2020-01-01T00:00:00+0200"); // ✅ "2020-01-01T00:00:00+02:00"
372-
datetime.parse("2020-01-01T00:00:00+02:00"); // ✅ "2020-01-01T00:00:00+02:00"
370+
// allows timezone offsets
371+
datetime.parse("2020-01-01T06:15:00+02:00"); //
372+
373+
// basic offsets not allowed
374+
datetime.parse("2020-01-01T06:15:00+02"); //
375+
datetime.parse("2020-01-01T06:15:00+0200"); //
373376

374377
// Z is still supported
375-
datetime.parse("2020-01-01T00:00:00Z"); //
378+
datetime.parse("2020-01-01T06:15:00Z"); //
376379
```
377380

378381
To allow unqualified (timezone-less) datetimes:
379382

380383
```ts
381384
const schema = z.iso.datetime({ local: true });
382-
schema.parse("2020-01-01T00:00:00"); //
385+
schema.parse("2020-01-01T06:15:01"); //
386+
schema.parse("2020-01-01T06:15"); // ✅ seconds optional
383387
```
384388

385-
To constrain the allowable `precision` (by default, arbitrary sub-second precision is supported).
389+
To constrain the allowable time `precision`. By default, seconds are optional and arbitrary sub-second precision is allowed.
386390

387391
```ts
388-
const datetime = z.iso.datetime({ precision: 3 });
392+
const a = z.iso.datetime();
393+
a.parse("2020-01-01T06:15Z"); //
394+
a.parse("2020-01-01T06:15:00Z"); //
395+
a.parse("2020-01-01T06:15:00.123Z"); //
396+
397+
const b = z.iso.datetime({ precision: -1 }); // minute precision (no seconds)
398+
b.parse("2020-01-01T06:15Z"); //
399+
b.parse("2020-01-01T06:15:00Z"); //
400+
b.parse("2020-01-01T06:15:00.123Z"); //
389401

390-
datetime.parse("2020-01-01T00:00:00.123Z"); //
391-
datetime.parse("2020-01-01T00:00:00Z"); //
392-
datetime.parse("2020-01-01T00:00:00.123456Z"); //
402+
const c = z.iso.datetime({ precision: 0 }); // second precision only
403+
c.parse("2020-01-01T06:15Z"); //
404+
c.parse("2020-01-01T06:15:00Z"); //
405+
c.parse("2020-01-01T06:15:00.123Z"); //
406+
407+
const d = z.iso.datetime({ precision: 3 }); // millisecond precision only
408+
d.parse("2020-01-01T06:15Z"); //
409+
d.parse("2020-01-01T06:15:00Z"); //
410+
d.parse("2020-01-01T06:15:00.123Z"); //
393411
```
394412

395413
### ISO dates
@@ -406,29 +424,31 @@ date.parse("2020-01-32"); // ❌
406424

407425
### ISO times
408426

409-
> Added in Zod 3.23
410-
411-
The `z.iso.time()` method validates strings in the format `HH:MM:SS[.s+]`. The second can include arbitrary decimal precision. It does not allow timezone offsets of any kind.
427+
The `z.iso.time()` method validates strings in the format `HH:MM[:SS[.s+]]`. By default seconds are optional, as are sub-second deciams.
412428

413429
```ts
414430
const time = z.iso.time();
415431

416-
time.parse("00:00:00"); //
417-
time.parse("09:52:31"); //
418-
time.parse("23:59:59.9999999"); // ✅ (arbitrary precision)
419-
420-
time.parse("00:00:00.123Z"); // ❌ (no `Z` allowed)
421-
time.parse("00:00:00.123+02:00"); // ❌ (no offsets allowed)
432+
time.parse("03:15"); //
433+
time.parse("03:15:00"); //
434+
time.parse("03:15:00.9999999"); // ✅ (arbitrary precision)
422435
```
423436

424-
You can set the `precision` option to constrain the allowable decimal precision.
437+
No offsets of any kind are allowed.
425438

426439
```ts
427-
const time = z.iso.time({ precision: 3 });
440+
time.parse("03:15:00Z"); // ❌ (no `Z` allowed)
441+
time.parse("03:15:00+02:00"); // ❌ (no offsets allowed)
442+
```
428443

429-
time.parse("00:00:00.123"); //
430-
time.parse("00:00:00.123456"); //
431-
time.parse("00:00:00"); //
444+
Use the `precision` parameter to constrain the allowable decimal precision.
445+
446+
```ts
447+
z.iso.time({ precision: -1 }); // HH:MM (minute precision)
448+
z.iso.time({ precision: 0 }); // HH:MM:SS (second precision)
449+
z.iso.time({ precision: 1 }); // HH:MM:SS.s (decisecond precision)
450+
z.iso.time({ precision: 2 }); // HH:MM:SS.ss (centisecond precision)
451+
z.iso.time({ precision: 3 }); // HH:MM:SS.sss (millisecond precision)
432452
```
433453

434454
### IP addresses
@@ -597,7 +617,7 @@ Use `z.date()` to validate `Date` instances.
597617

598618
```ts
599619
z.date().safeParse(new Date()); // success: true
600-
z.date().safeParse("2022-01-12T00:00:00.000Z"); // success: false
620+
z.date().safeParse("2022-01-12T06:15:00.000Z"); // success: false
601621
```
602622

603623
To customize the error message:
@@ -1586,7 +1606,7 @@ const Person = z.record(Keys, z.string());
15861606
const myRecord: MyRecord = { a: "foo" }; // ❌ missing required key `b`
15871607
```
15881608

1589-
In Zod 3, exhaustiveness was not checked. To replicate the Zod 3 behavior, use `z.partialRecord()`.
1609+
In Zod 3, exhaustiveness was not checked. To replicate the old behavior, use `z.partialRecord()`.
15901610

15911611
</Callout>
15921612

packages/zod/src/v4/classic/external.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export {
2727
formatError,
2828
flattenError,
2929
toJSONSchema,
30+
TimePrecision,
3031
} from "zod/v4/core";
3132

3233
export * as locales from "../locales/index.js";

0 commit comments

Comments
 (0)