Skip to content

Commit a429256

Browse files
authored
Allow HH:MM format in z.string().datetime() and z.string().time() (#4315)
* feat: allow omitting seconds in time string * docs: include examples of omitted seconds * docs: remove whitespace
1 parent 301e5ca commit a429256

File tree

6 files changed

+68
-34
lines changed

6 files changed

+68
-34
lines changed

README.md

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@
5353
<!-- <hr/> -->
5454
<br/>
5555
<br/>
56-
56+
5757
## Table of contents
5858

5959
> These docs have been translated into [Chinese](./README_ZH.md) and [Korean](./README_KO.md).
@@ -216,7 +216,7 @@ Sponsorship at any level is appreciated and encouraged. If you built a paid prod
216216
<img alt="CodeRabbit logo" height="80px" src="https://github.com/user-attachments/assets/d791bc7d-dc60-4d55-9c31-97779839cb74">
217217
</picture>
218218
</a>
219-
<br />
219+
<br />
220220
Cut code review time & bugs in half
221221
<br/>
222222
<a href="https://www.coderabbit.ai/" style="text-decoration:none;">coderabbit.ai</a>
@@ -241,7 +241,7 @@ Sponsorship at any level is appreciated and encouraged. If you built a paid prod
241241
<img alt="Courier logo" height="62px" src="https://github.com/user-attachments/assets/6b09506a-78de-47e8-a8c1-792efe31910a">
242242
</picture>
243243
</a>
244-
<br />
244+
<br />
245245
The API platform for sending notifications
246246
<br/>
247247
<a href="https://www.courier.com/?utm_source=zod&utm_campaign=osssponsors" style="text-decoration:none;">courier.com</a>
@@ -257,7 +257,7 @@ Sponsorship at any level is appreciated and encouraged. If you built a paid prod
257257
<img alt="LibLab" height="62px" src="https://github.com/user-attachments/assets/3de0b617-5137-49c4-b72d-a033cbe602d8">
258258
</picture>
259259
</a>
260-
<br />
260+
<br />
261261
Generate better SDKs for your APIs
262262
<br/>
263263
<a href="https://liblab.com/?utm_source=zod" style="text-decoration:none;">liblab.com</a>
@@ -275,7 +275,7 @@ Sponsorship at any level is appreciated and encouraged. If you built a paid prod
275275
<img alt="Neon" height="68px" src="https://github.com/user-attachments/assets/b5799fc8-81ff-4053-a1c3-b29adf85e7a1">
276276
</picture>
277277
</a>
278-
<br />
278+
<br />
279279
Serverless Postgres — Ship faster
280280
<br/>
281281
<a href="https://neon.tech" style="text-decoration:none;">neon.tech</a>
@@ -291,7 +291,7 @@ Sponsorship at any level is appreciated and encouraged. If you built a paid prod
291291
<img alt="Retool" height="45px" src="https://github.com/colinhacks/zod/assets/3084745/5ef4c11b-efeb-4495-90a8-41b83f798600">
292292
</picture>
293293
</a>
294-
<br />
294+
<br />
295295
Build AI apps and workflows with <a href="https://retool.com/products/ai?utm_source=github&utm_medium=referral&utm_campaign=zod">Retool AI</a>
296296
<br/>
297297
<a href="https://retool.com/?utm_source=github&utm_medium=referral&utm_campaign=zod" style="text-decoration:none;">retool.com</a>
@@ -309,7 +309,7 @@ Sponsorship at any level is appreciated and encouraged. If you built a paid prod
309309
<img alt="stainless" height="45px" src="https://github.com/colinhacks/zod/assets/3084745/e9444e44-d991-4bba-a697-dbcfad608e47">
310310
</picture>
311311
</a>
312-
<br />
312+
<br />
313313
Generate best-in-class SDKs
314314
<br/>
315315
<a href="https://stainless.com" style="text-decoration:none;">stainless.com</a>
@@ -325,7 +325,7 @@ Sponsorship at any level is appreciated and encouraged. If you built a paid prod
325325
<img alt="speakeasy" height="40px" src="https://github.com/colinhacks/zod/assets/3084745/647524a4-22bb-4199-be70-404207a5a2b5">
326326
</picture>
327327
</a>
328-
<br />
328+
<br />
329329
SDKs & Terraform providers for your API
330330
<br/>
331331
<a href="https://speakeasy.com/?utm_source=zod+docs" style="text-decoration:none;">speakeasy.com</a>
@@ -847,7 +847,7 @@ z.string().toUpperCase(); // toUpperCase
847847

848848
// added in Zod 3.23
849849
z.string().date(); // ISO date format (YYYY-MM-DD)
850-
z.string().time(); // ISO time format (HH:mm:ss[.SSSSSS])
850+
z.string().time(); // ISO time format (HH:mm:ss[.SSSSSS] or HH:mm)
851851
z.string().duration(); // ISO 8601 duration
852852
z.string().base64();
853853
```
@@ -887,14 +887,15 @@ z.string().cidr({ message: "Invalid CIDR" });
887887

888888
As you may have noticed, Zod string includes a few date/time related validations. These validations are regular expression based, so they are not as strict as a full date/time library. However, they are very convenient for validating user input.
889889

890-
The `z.string().datetime()` method enforces ISO 8601; default is no timezone offsets and arbitrary sub-second decimal precision.
890+
The `z.string().datetime()` method enforces ISO 8601; default is no timezone offsets and arbitrary sub-second decimal precision. Seconds may be omitted if precision is not set.
891891

892892
```ts
893893
const datetime = z.string().datetime();
894894

895895
datetime.parse("2020-01-01T00:00:00Z"); // pass
896896
datetime.parse("2020-01-01T00:00:00.123Z"); // pass
897897
datetime.parse("2020-01-01T00:00:00.123456Z"); // pass (arbitrary precision)
898+
datetime.parse("2020-01-01T00:00Z"); // pass (hours and minutes only)
898899
datetime.parse("2020-01-01T00:00:00+02:00"); // fail (no offsets allowed)
899900
```
900901

@@ -904,6 +905,7 @@ Timezone offsets can be allowed by setting the `offset` option to `true`.
904905
const datetime = z.string().datetime({ offset: true });
905906

906907
datetime.parse("2020-01-01T00:00:00+02:00"); // pass
908+
datetime.parse("2020-01-01T00:00+02:00"); // pass
907909
datetime.parse("2020-01-01T00:00:00.123+02:00"); // pass (millis optional)
908910
datetime.parse("2020-01-01T00:00:00.123+0200"); // pass (millis optional)
909911
datetime.parse("2020-01-01T00:00:00.123+02"); // pass (only offset hours)
@@ -915,6 +917,7 @@ Allow unqualified (timezone-less) datetimes with the `local` flag.
915917
```ts
916918
const schema = z.string().datetime({ local: true });
917919
schema.parse("2020-01-01T00:00:00"); // pass
920+
schema.parse("2020-01-01T00:00"); // pass
918921
```
919922

920923
You can additionally constrain the allowable `precision`. By default, arbitrary sub-second precision is supported (but optional).
@@ -924,6 +927,7 @@ const datetime = z.string().datetime({ precision: 3 });
924927

925928
datetime.parse("2020-01-01T00:00:00.123Z"); // pass
926929
datetime.parse("2020-01-01T00:00:00Z"); // fail
930+
datetime.parse("2020-01-01T00:00Z"); // fail
927931
datetime.parse("2020-01-01T00:00:00.123456Z"); // fail
928932
```
929933

@@ -945,13 +949,14 @@ date.parse("2020-01-32"); // fail
945949

946950
> Added in Zod 3.23
947951
948-
The `z.string().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.
952+
The `z.string().time()` method validates strings in the format `HH:MM` or `HH:MM:SS[.s+]`. The second can include arbitrary decimal precision. It does not allow timezone offsets of any kind.
949953

950954
```ts
951955
const time = z.string().time();
952956

953957
time.parse("00:00:00"); // pass
954958
time.parse("09:52:31"); // pass
959+
time.parse("09:52"); // pass
955960
time.parse("23:59:59.9999999"); // pass (arbitrary precision)
956961

957962
time.parse("00:00:00.123Z"); // fail (no `Z` allowed)
@@ -966,6 +971,7 @@ const time = z.string().time({ precision: 3 });
966971
time.parse("00:00:00.123"); // pass
967972
time.parse("00:00:00.123456"); // fail
968973
time.parse("00:00:00"); // fail
974+
time.parse("00:00"); // fail
969975
```
970976

971977
### IP addresses

deno/lib/README.md

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@
5353
<!-- <hr/> -->
5454
<br/>
5555
<br/>
56-
56+
5757
## Table of contents
5858

5959
> These docs have been translated into [Chinese](./README_ZH.md) and [Korean](./README_KO.md).
@@ -216,7 +216,7 @@ Sponsorship at any level is appreciated and encouraged. If you built a paid prod
216216
<img alt="CodeRabbit logo" height="80px" src="https://github.com/user-attachments/assets/d791bc7d-dc60-4d55-9c31-97779839cb74">
217217
</picture>
218218
</a>
219-
<br />
219+
<br />
220220
Cut code review time & bugs in half
221221
<br/>
222222
<a href="https://www.coderabbit.ai/" style="text-decoration:none;">coderabbit.ai</a>
@@ -241,7 +241,7 @@ Sponsorship at any level is appreciated and encouraged. If you built a paid prod
241241
<img alt="Courier logo" height="62px" src="https://github.com/user-attachments/assets/6b09506a-78de-47e8-a8c1-792efe31910a">
242242
</picture>
243243
</a>
244-
<br />
244+
<br />
245245
The API platform for sending notifications
246246
<br/>
247247
<a href="https://www.courier.com/?utm_source=zod&utm_campaign=osssponsors" style="text-decoration:none;">courier.com</a>
@@ -257,7 +257,7 @@ Sponsorship at any level is appreciated and encouraged. If you built a paid prod
257257
<img alt="LibLab" height="62px" src="https://github.com/user-attachments/assets/3de0b617-5137-49c4-b72d-a033cbe602d8">
258258
</picture>
259259
</a>
260-
<br />
260+
<br />
261261
Generate better SDKs for your APIs
262262
<br/>
263263
<a href="https://liblab.com/?utm_source=zod" style="text-decoration:none;">liblab.com</a>
@@ -275,7 +275,7 @@ Sponsorship at any level is appreciated and encouraged. If you built a paid prod
275275
<img alt="Neon" height="68px" src="https://github.com/user-attachments/assets/b5799fc8-81ff-4053-a1c3-b29adf85e7a1">
276276
</picture>
277277
</a>
278-
<br />
278+
<br />
279279
Serverless Postgres — Ship faster
280280
<br/>
281281
<a href="https://neon.tech" style="text-decoration:none;">neon.tech</a>
@@ -291,7 +291,7 @@ Sponsorship at any level is appreciated and encouraged. If you built a paid prod
291291
<img alt="Retool" height="45px" src="https://github.com/colinhacks/zod/assets/3084745/5ef4c11b-efeb-4495-90a8-41b83f798600">
292292
</picture>
293293
</a>
294-
<br />
294+
<br />
295295
Build AI apps and workflows with <a href="https://retool.com/products/ai?utm_source=github&utm_medium=referral&utm_campaign=zod">Retool AI</a>
296296
<br/>
297297
<a href="https://retool.com/?utm_source=github&utm_medium=referral&utm_campaign=zod" style="text-decoration:none;">retool.com</a>
@@ -309,7 +309,7 @@ Sponsorship at any level is appreciated and encouraged. If you built a paid prod
309309
<img alt="stainless" height="45px" src="https://github.com/colinhacks/zod/assets/3084745/e9444e44-d991-4bba-a697-dbcfad608e47">
310310
</picture>
311311
</a>
312-
<br />
312+
<br />
313313
Generate best-in-class SDKs
314314
<br/>
315315
<a href="https://stainless.com" style="text-decoration:none;">stainless.com</a>
@@ -325,7 +325,7 @@ Sponsorship at any level is appreciated and encouraged. If you built a paid prod
325325
<img alt="speakeasy" height="40px" src="https://github.com/colinhacks/zod/assets/3084745/647524a4-22bb-4199-be70-404207a5a2b5">
326326
</picture>
327327
</a>
328-
<br />
328+
<br />
329329
SDKs & Terraform providers for your API
330330
<br/>
331331
<a href="https://speakeasy.com/?utm_source=zod+docs" style="text-decoration:none;">speakeasy.com</a>
@@ -847,7 +847,7 @@ z.string().toUpperCase(); // toUpperCase
847847

848848
// added in Zod 3.23
849849
z.string().date(); // ISO date format (YYYY-MM-DD)
850-
z.string().time(); // ISO time format (HH:mm:ss[.SSSSSS])
850+
z.string().time(); // ISO time format (HH:mm:ss[.SSSSSS] or HH:mm)
851851
z.string().duration(); // ISO 8601 duration
852852
z.string().base64();
853853
```
@@ -887,14 +887,15 @@ z.string().cidr({ message: "Invalid CIDR" });
887887

888888
As you may have noticed, Zod string includes a few date/time related validations. These validations are regular expression based, so they are not as strict as a full date/time library. However, they are very convenient for validating user input.
889889

890-
The `z.string().datetime()` method enforces ISO 8601; default is no timezone offsets and arbitrary sub-second decimal precision.
890+
The `z.string().datetime()` method enforces ISO 8601; default is no timezone offsets and arbitrary sub-second decimal precision. Seconds may be omitted if precision is not set.
891891

892892
```ts
893893
const datetime = z.string().datetime();
894894

895895
datetime.parse("2020-01-01T00:00:00Z"); // pass
896896
datetime.parse("2020-01-01T00:00:00.123Z"); // pass
897897
datetime.parse("2020-01-01T00:00:00.123456Z"); // pass (arbitrary precision)
898+
datetime.parse("2020-01-01T00:00Z"); // pass (hours and minutes only)
898899
datetime.parse("2020-01-01T00:00:00+02:00"); // fail (no offsets allowed)
899900
```
900901

@@ -904,6 +905,7 @@ Timezone offsets can be allowed by setting the `offset` option to `true`.
904905
const datetime = z.string().datetime({ offset: true });
905906

906907
datetime.parse("2020-01-01T00:00:00+02:00"); // pass
908+
datetime.parse("2020-01-01T00:00+02:00"); // pass
907909
datetime.parse("2020-01-01T00:00:00.123+02:00"); // pass (millis optional)
908910
datetime.parse("2020-01-01T00:00:00.123+0200"); // pass (millis optional)
909911
datetime.parse("2020-01-01T00:00:00.123+02"); // pass (only offset hours)
@@ -915,6 +917,7 @@ Allow unqualified (timezone-less) datetimes with the `local` flag.
915917
```ts
916918
const schema = z.string().datetime({ local: true });
917919
schema.parse("2020-01-01T00:00:00"); // pass
920+
schema.parse("2020-01-01T00:00"); // pass
918921
```
919922

920923
You can additionally constrain the allowable `precision`. By default, arbitrary sub-second precision is supported (but optional).
@@ -924,6 +927,7 @@ const datetime = z.string().datetime({ precision: 3 });
924927

925928
datetime.parse("2020-01-01T00:00:00.123Z"); // pass
926929
datetime.parse("2020-01-01T00:00:00Z"); // fail
930+
datetime.parse("2020-01-01T00:00Z"); // fail
927931
datetime.parse("2020-01-01T00:00:00.123456Z"); // fail
928932
```
929933

@@ -945,13 +949,14 @@ date.parse("2020-01-32"); // fail
945949

946950
> Added in Zod 3.23
947951
948-
The `z.string().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.
952+
The `z.string().time()` method validates strings in the format `HH:MM` or `HH:MM:SS[.s+]`. The second can include arbitrary decimal precision. It does not allow timezone offsets of any kind.
949953

950954
```ts
951955
const time = z.string().time();
952956

953957
time.parse("00:00:00"); // pass
954958
time.parse("09:52:31"); // pass
959+
time.parse("09:52"); // pass
955960
time.parse("23:59:59.9999999"); // pass (arbitrary precision)
956961

957962
time.parse("00:00:00.123Z"); // fail (no `Z` allowed)
@@ -966,6 +971,7 @@ const time = z.string().time({ precision: 3 });
966971
time.parse("00:00:00.123"); // pass
967972
time.parse("00:00:00.123456"); // fail
968973
time.parse("00:00:00"); // fail
974+
time.parse("00:00"); // fail
969975
```
970976

971977
### IP addresses

deno/lib/__tests__/string.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -594,15 +594,18 @@ test("datetime parsing", () => {
594594
datetime.parse("2022-10-13T09:52:31.8162314Z");
595595
datetime.parse("1970-01-01T00:00:00Z");
596596
datetime.parse("2022-10-13T09:52:31Z");
597+
datetime.parse("2022-10-13T09:52Z");
597598
expect(() => datetime.parse("")).toThrow();
598599
expect(() => datetime.parse("foo")).toThrow();
599600
expect(() => datetime.parse("2020-10-14")).toThrow();
600601
expect(() => datetime.parse("T18:45:12.123")).toThrow();
601602
expect(() => datetime.parse("2020-10-14T17:42:29+00:00")).toThrow();
603+
expect(() => datetime.parse("2020-10-14T17:42.123+00:00")).toThrow();
602604

603605
const datetimeNoMs = z.string().datetime({ precision: 0 });
604606
datetimeNoMs.parse("1970-01-01T00:00:00Z");
605607
datetimeNoMs.parse("2022-10-13T09:52:31Z");
608+
datetimeNoMs.parse("2022-10-13T09:52Z");
606609
expect(() => datetimeNoMs.parse("tuna")).toThrow();
607610
expect(() => datetimeNoMs.parse("1970-01-01T00:00:00.000Z")).toThrow();
608611
expect(() => datetimeNoMs.parse("1970-01-01T00:00:00.Z")).toThrow();
@@ -615,6 +618,7 @@ test("datetime parsing", () => {
615618
expect(() => datetime3Ms.parse("1970-01-01T00:00:00.1Z")).toThrow();
616619
expect(() => datetime3Ms.parse("1970-01-01T00:00:00.12Z")).toThrow();
617620
expect(() => datetime3Ms.parse("2022-10-13T09:52:31Z")).toThrow();
621+
expect(() => datetime3Ms.parse("2022-10-13T09:52Z")).toThrow();
618622

619623
const datetimeOffset = z.string().datetime({ offset: true });
620624
datetimeOffset.parse("1970-01-01T00:00:00.000Z");
@@ -624,6 +628,7 @@ test("datetime parsing", () => {
624628
datetimeOffset.parse("2020-10-14T17:42:29+00:00");
625629
datetimeOffset.parse("2020-10-14T17:42:29+03:15");
626630
datetimeOffset.parse("2020-10-14T17:42:29+0315");
631+
datetimeOffset.parse("2020-10-14T17:42+0315");
627632
expect(() => datetimeOffset.parse("2020-10-14T17:42:29+03"));
628633
expect(() => datetimeOffset.parse("tuna")).toThrow();
629634
expect(() => datetimeOffset.parse("2022-10-13T09:52:31.Z")).toThrow();
@@ -635,6 +640,7 @@ test("datetime parsing", () => {
635640
datetimeOffsetNoMs.parse("2022-10-13T09:52:31Z");
636641
datetimeOffsetNoMs.parse("2020-10-14T17:42:29+00:00");
637642
datetimeOffsetNoMs.parse("2020-10-14T17:42:29+0000");
643+
datetimeOffsetNoMs.parse("2020-10-14T17:42+0000");
638644
expect(() => datetimeOffsetNoMs.parse("2020-10-14T17:42:29+00")).toThrow();
639645
expect(() => datetimeOffsetNoMs.parse("tuna")).toThrow();
640646
expect(() => datetimeOffsetNoMs.parse("1970-01-01T00:00:00.000Z")).toThrow();
@@ -656,6 +662,7 @@ test("datetime parsing", () => {
656662
expect(() =>
657663
datetimeOffset4Ms.parse("2020-10-14T17:42:29.124+00:00")
658664
).toThrow();
665+
expect(() => datetimeOffset4Ms.parse("2020-10-14T17:42+00:00")).toThrow();
659666
});
660667

661668
test("date", () => {
@@ -731,6 +738,7 @@ test("time parsing", () => {
731738
time.parse("23:59:59");
732739
time.parse("09:52:31");
733740
time.parse("23:59:59.9999999");
741+
time.parse("23:59");
734742
expect(() => time.parse("")).toThrow();
735743
expect(() => time.parse("foo")).toThrow();
736744
expect(() => time.parse("00:00:00Z")).toThrow();
@@ -743,6 +751,7 @@ test("time parsing", () => {
743751
expect(() => time.parse("00:60:00")).toThrow();
744752
expect(() => time.parse("00:00:60")).toThrow();
745753
expect(() => time.parse("24:60:60")).toThrow();
754+
expect(() => time.parse("24:60")).toThrow();
746755

747756
const time2 = z.string().time({ precision: 2 });
748757
time2.parse("00:00:00.00");
@@ -755,6 +764,7 @@ test("time parsing", () => {
755764
expect(() => time2.parse("00:00:00.0")).toThrow();
756765
expect(() => time2.parse("00:00:00.000")).toThrow();
757766
expect(() => time2.parse("00:00:00.00+00:00")).toThrow();
767+
expect(() => time2.parse("23:59")).toThrow();
758768

759769
// const time3 = z.string().time({ offset: true });
760770
// time3.parse("00:00:00Z");

deno/lib/types.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -696,16 +696,17 @@ const dateRegexSource = `((\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[0246
696696
const dateRegex = new RegExp(`^${dateRegexSource}$`);
697697

698698
function timeRegexSource(args: { precision?: number | null }) {
699-
// let regex = `\\d{2}:\\d{2}:\\d{2}`;
700-
let regex = `([01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d`;
701-
699+
let secondsRegexSource = `[0-5]\\d`;
702700
if (args.precision) {
703-
regex = `${regex}\\.\\d{${args.precision}}`;
701+
secondsRegexSource = `${secondsRegexSource}\\.\\d{${args.precision}}`;
704702
} else if (args.precision == null) {
705-
regex = `${regex}(\\.\\d+)?`;
703+
secondsRegexSource = `${secondsRegexSource}(\\.\\d+)?`;
706704
}
707-
return regex;
705+
706+
const secondsQuantifier = args.precision ? "+" : "?"; // require seconds if precision is nonzero
707+
return `([01]\\d|2[0-3]):[0-5]\\d(:${secondsRegexSource})${secondsQuantifier}`;
708708
}
709+
709710
function timeRegex(args: {
710711
offset?: boolean;
711712
local?: boolean;

0 commit comments

Comments
 (0)