Skip to content

Commit 7ed0c36

Browse files
authored
Allow hours-only offsets. Normalize. (#4676)
1 parent 1614fd8 commit 7ed0c36

File tree

5 files changed

+50
-6
lines changed

5 files changed

+50
-6
lines changed

packages/docs/content/api.mdx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -366,11 +366,13 @@ To allow timezone offsets:
366366
```ts
367367
const datetime = z.iso.datetime({ offset: true });
368368

369-
datetime.parse("2020-01-01T00:00:00+02:00"); //
370-
datetime.parse("2020-01-01T00:00:00.123+02:00"); // ✅ (millis optional)
371-
datetime.parse("2020-01-01T00:00:00.123+0200"); // ✅ (millis optional)
372-
datetime.parse("2020-01-01T00:00:00.123+02"); // ✅ (only offset hours)
373-
datetime.parse("2020-01-01T00:00:00Z"); // ✅ (Z still supported)
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"
373+
374+
// Z is still supported
375+
datetime.parse("2020-01-01T00:00:00Z"); //
374376
```
375377

376378
To allow unqualified (timezone-less) datetimes:

packages/zod/src/v4/classic/tests/string.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -816,6 +816,21 @@ test("datetime parsing", () => {
816816
expect(() => datetimeOffset4Ms.parse("2020-10-14T17:42:29.124+00:00")).toThrow();
817817
});
818818

819+
test("datetime offset normalization", () => {
820+
const a = z.iso.datetime({ offset: true });
821+
expect({
822+
a: a.parse("2020-10-14T17:42:29+02"),
823+
b: a.parse("2020-10-14T17:42:29+0200"),
824+
c: a.parse("2020-10-14T17:42:29+02:00"),
825+
}).toMatchInlineSnapshot(`
826+
{
827+
"a": "2020-10-14T17:42:29+02:00",
828+
"b": "2020-10-14T17:42:29+02:00",
829+
"c": "2020-10-14T17:42:29+02:00",
830+
}
831+
`);
832+
});
833+
819834
test("date parsing", () => {
820835
const date = z.string().date();
821836
date.parse("1970-01-01");

packages/zod/src/v4/core/regexes.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,9 @@ export function datetime(args: {
107107

108108
const opts: string[] = [];
109109
opts.push(args.local ? `Z?` : `Z`);
110-
if (args.offset) opts.push(`([+-]\\d{2}:?\\d{2})`);
110+
// if (args.offset) opts.push(`([+-]\\d{2}:?\\d{2})`);
111+
// minutes, colon optional
112+
if (args.offset) opts.push(`([+-]\\d{2}(?::?\\d{2})?)`);
111113
regex = `${regex}(${opts.join("|")})`;
112114
return new RegExp(`^${regex}$`);
113115
}

packages/zod/src/v4/core/schemas.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -604,6 +604,20 @@ export const $ZodISODateTime: core.$constructor<$ZodISODateTime> = /*@__PURE__*/
604604
(inst, def): void => {
605605
def.pattern ??= regexes.datetime(def);
606606
$ZodStringFormat.init(inst, def);
607+
608+
const _super = inst._zod.check;
609+
inst._zod.check = (payload) => {
610+
_super(payload);
611+
612+
// normalize timezone offset
613+
// add colon & minutes if missing
614+
// if no offset, return early
615+
const curr = payload.value;
616+
if (/[+-]\d\d$/.test(curr)) payload.value = curr + ":00";
617+
else if (/[+-]\d\d\d\d$/.test(curr)) {
618+
payload.value = curr.slice(0, -2) + ":" + curr.slice(-2);
619+
}
620+
};
607621
}
608622
);
609623

play.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
11
import { z } from "zod/v4";
22

33
z;
4+
5+
// const schema = z.iso.datetime({ offset: true, local: true });
6+
// console.dir(schema.parse("2023-10-01T12:00:00.132"), { depth: null });
7+
8+
const datetime = z.iso.datetime({ offset: true });
9+
10+
datetime.parse("2020-01-01T00:00:00+02:00"); // ✅
11+
datetime.parse("2020-01-01T00:00:00.123+02:00"); // ✅ (millis optional)
12+
datetime.parse("2020-01-01T00:00:00.123+0200"); // ✅ (millis optional)
13+
datetime.parse("2020-01-01T00:00:00.123+02"); // ✅ (only offset hours)
14+
datetime.parse("2020-01-01T00:00:00Z"); // ✅ (Z still supported)

0 commit comments

Comments
 (0)