Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add ZodString.date() & ZodString.time(). #1766

Merged
merged 20 commits into from
Apr 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
47c7fa4
implement `ZodString.date()` & `ZodString.time()`.
igalklebanov Dec 27, 2022
6280b3c
further simplify datetime regex building func.
igalklebanov Dec 27, 2022
8c3081c
Merge branch 'master' into string-date-time
igalklebanov Jan 15, 2023
c55f7ad
add tests for `ZodString.date()` & `ZodString.time()`.
igalklebanov Jan 15, 2023
86d7066
add `ZodString.date()` & `ZodString.time()` to README.md.
igalklebanov Jan 15, 2023
9e5579e
update README.md with time and date stuff, and strictness disclaimer.
igalklebanov Jan 20, 2023
347907e
update README.md with time and date stuff, and strictness disclaimer. p2
igalklebanov Jan 20, 2023
0bd433d
make 0 precision case clearer for ZodString time checks.
igalklebanov Jan 20, 2023
18de4ef
Merge branch 'master' into string-date-time
igalklebanov Jan 29, 2023
65be470
react to merge of #1797.
igalklebanov Jan 29, 2023
9d24251
Merge branch 'master' into string-date-time
igalklebanov Jan 29, 2023
14f395a
Merge branch 'master' into string-date-time
igalklebanov Feb 16, 2023
986c54d
Merge branch 'master' into string-date-time
igalklebanov Feb 27, 2023
a6b1f0b
fix syntax error following merge commit.
igalklebanov Feb 27, 2023
10da48c
prettier string test.
igalklebanov Feb 27, 2023
dfa2907
Merge branch 'master' into string-date-time
igalklebanov Mar 9, 2023
7658f89
Merge branch 'colinhacks:master' into string-date-time
igalklebanov May 22, 2023
0395f45
Merge branch 'colinhacks:master' into string-date-time
igalklebanov Aug 7, 2023
b530436
Merge branch 'master' into string-date-time
igalklebanov Nov 30, 2023
5328a97
prettier le readme.
igalklebanov Nov 30, 2023
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
42 changes: 41 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -735,6 +735,8 @@ z.string().includes(string);
z.string().startsWith(string);
z.string().endsWith(string);
z.string().datetime(); // ISO 8601; default is without UTC offset, see below for options
z.string().date(); // ISO short date format.
z.string().time(); // time of day in 24-hour format, see below for options.
z.string().ip(); // defaults to IPv4 and IPv6, see below for options

// transformations
Expand Down Expand Up @@ -768,10 +770,23 @@ z.string().includes("tuna", { message: "Must include tuna" });
z.string().startsWith("https://", { message: "Must provide secure URL" });
z.string().endsWith(".com", { message: "Only .com domains allowed" });
z.string().datetime({ message: "Invalid datetime string! Must be UTC." });
z.string().date({ message: "Invalid date string!" });
z.string().time({ message: "Invalid time string!" });
z.string().ip({ message: "Invalid IP address" });
```

### ISO datetimes
### ISO Date, Time & Datetime validation

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.

The `z.string().date()` method validates strings in the format `YYYY-MM-DD`.

The `z.string().time()` method validates strings in the format `HH:mm:ss[.SSSSSS][Z|(+|-)hh[:]mm]`
(the time portion of [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601)). It defaults
to `HH:mm:ss[.SSSSSS]` validation: no timezone offsets or `Z`, with arbitrary sub-second
decimal.

The `z.string().datetime()` method enforces ISO 8601; default is no timezone offsets and arbitrary sub-second decimal precision.

Expand All @@ -782,6 +797,18 @@ datetime.parse("2020-01-01T00:00:00Z"); // pass
datetime.parse("2020-01-01T00:00:00.123Z"); // pass
datetime.parse("2020-01-01T00:00:00.123456Z"); // pass (arbitrary precision)
datetime.parse("2020-01-01T00:00:00+02:00"); // fail (no offsets allowed)

const date = z.string().date();

date.parse("2020-01-01"); // pass

const time = z.string().time();

time.parse("00:00:00"); // pass
time.parse("09:52:31"); // pass
time.parse("23:59:59.9999999"); // pass (arbitrary precision)
time.parse("00:00:00.123Z"); // fail (no `Z` allowed)
time.parse("00:00:00.123+02:00"); // fail (no offsets allowed)
```

Timezone offsets can be allowed by setting the `offset` option to `true`.
Expand All @@ -794,6 +821,13 @@ datetime.parse("2020-01-01T00:00:00.123+02:00"); // pass (millis optional)
datetime.parse("2020-01-01T00:00:00.123+0200"); // pass (millis optional)
datetime.parse("2020-01-01T00:00:00.123+02"); // pass (only offset hours)
datetime.parse("2020-01-01T00:00:00Z"); // pass (Z still supported)

const time = z.string().time({ offset: true });

time.parse("00:00:00+02:00"); // pass
time.parse("00:00:00.123+02:00"); // pass (millis optional)
time.parse("00:00:00.123+0200"); // pass (millis optional)
time.parse("00:00:00Z"); // pass (`Z` now supported)
```

You can additionally constrain the allowable `precision`. By default, arbitrary sub-second precision is supported (but optional).
Expand All @@ -804,6 +838,12 @@ const datetime = z.string().datetime({ precision: 3 });
datetime.parse("2020-01-01T00:00:00.123Z"); // pass
datetime.parse("2020-01-01T00:00:00Z"); // fail
datetime.parse("2020-01-01T00:00:00.123456Z"); // fail

const time = z.string().time({ precision: 3 });

time.parse("00:00:00.123"); // pass
time.parse("00:00:00"); // fail
time.parse("00:00:00.123456"); // fail
```

### IP addresses
Expand Down
42 changes: 41 additions & 1 deletion deno/lib/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -735,6 +735,8 @@ z.string().includes(string);
z.string().startsWith(string);
z.string().endsWith(string);
z.string().datetime(); // ISO 8601; default is without UTC offset, see below for options
z.string().date(); // ISO short date format.
z.string().time(); // time of day in 24-hour format, see below for options.
z.string().ip(); // defaults to IPv4 and IPv6, see below for options

// transformations
Expand Down Expand Up @@ -768,10 +770,23 @@ z.string().includes("tuna", { message: "Must include tuna" });
z.string().startsWith("https://", { message: "Must provide secure URL" });
z.string().endsWith(".com", { message: "Only .com domains allowed" });
z.string().datetime({ message: "Invalid datetime string! Must be UTC." });
z.string().date({ message: "Invalid date string!" });
z.string().time({ message: "Invalid time string!" });
z.string().ip({ message: "Invalid IP address" });
```

### ISO datetimes
### ISO Date, Time & Datetime validation

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.

The `z.string().date()` method validates strings in the format `YYYY-MM-DD`.

The `z.string().time()` method validates strings in the format `HH:mm:ss[.SSSSSS][Z|(+|-)hh[:]mm]`
(the time portion of [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601)). It defaults
to `HH:mm:ss[.SSSSSS]` validation: no timezone offsets or `Z`, with arbitrary sub-second
decimal.

The `z.string().datetime()` method enforces ISO 8601; default is no timezone offsets and arbitrary sub-second decimal precision.

Expand All @@ -782,6 +797,18 @@ datetime.parse("2020-01-01T00:00:00Z"); // pass
datetime.parse("2020-01-01T00:00:00.123Z"); // pass
datetime.parse("2020-01-01T00:00:00.123456Z"); // pass (arbitrary precision)
datetime.parse("2020-01-01T00:00:00+02:00"); // fail (no offsets allowed)

const date = z.string().date();

date.parse("2020-01-01"); // pass

const time = z.string().time();

time.parse("00:00:00"); // pass
time.parse("09:52:31"); // pass
time.parse("23:59:59.9999999"); // pass (arbitrary precision)
time.parse("00:00:00.123Z"); // fail (no `Z` allowed)
time.parse("00:00:00.123+02:00"); // fail (no offsets allowed)
```

Timezone offsets can be allowed by setting the `offset` option to `true`.
Expand All @@ -794,6 +821,13 @@ datetime.parse("2020-01-01T00:00:00.123+02:00"); // pass (millis optional)
datetime.parse("2020-01-01T00:00:00.123+0200"); // pass (millis optional)
datetime.parse("2020-01-01T00:00:00.123+02"); // pass (only offset hours)
datetime.parse("2020-01-01T00:00:00Z"); // pass (Z still supported)

const time = z.string().time({ offset: true });

time.parse("00:00:00+02:00"); // pass
time.parse("00:00:00.123+02:00"); // pass (millis optional)
time.parse("00:00:00.123+0200"); // pass (millis optional)
time.parse("00:00:00Z"); // pass (`Z` now supported)
```

You can additionally constrain the allowable `precision`. By default, arbitrary sub-second precision is supported (but optional).
Expand All @@ -804,6 +838,12 @@ const datetime = z.string().datetime({ precision: 3 });
datetime.parse("2020-01-01T00:00:00.123Z"); // pass
datetime.parse("2020-01-01T00:00:00Z"); // fail
datetime.parse("2020-01-01T00:00:00.123456Z"); // fail

const time = z.string().time({ precision: 3 });

time.parse("00:00:00.123"); // pass
time.parse("00:00:00"); // fail
time.parse("00:00:00.123456"); // fail
```

### IP addresses
Expand Down
2 changes: 2 additions & 0 deletions deno/lib/ZodError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ export type StringValidation =
| "cuid2"
| "ulid"
| "datetime"
| "date"
| "time"
| "ip"
| { includes: string; position?: number }
| { startsWith: string }
Expand Down
82 changes: 82 additions & 0 deletions deno/lib/__tests__/string.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,88 @@ test("datetime parsing", () => {
).toThrow();
});

test("date", () => {
const a = z.string().date();
expect(a.isDate).toEqual(true);
});

test("date parsing", () => {
const date = z.string().date();
date.parse("1970-01-01");
date.parse("2022-10-13");
expect(() => date.parse("")).toThrow();
expect(() => date.parse("foo")).toThrow();
expect(() => date.parse("200-01-01")).toThrow();
expect(() => date.parse("20000-01-01")).toThrow();
expect(() => date.parse("2000-0-01")).toThrow();
expect(() => date.parse("2000-011-01")).toThrow();
expect(() => date.parse("2000-01-0")).toThrow();
expect(() => date.parse("2000-01-011")).toThrow();
expect(() => date.parse("2000/01/01")).toThrow();
expect(() => date.parse("01-01-2022")).toThrow();
expect(() => date.parse("01/01/2022")).toThrow();
expect(() => date.parse("2000-01-01 00:00:00Z")).toThrow();
expect(() => date.parse("2020-10-14T17:42:29+00:00")).toThrow();
expect(() => date.parse("2020-10-14T17:42:29Z")).toThrow();
expect(() => date.parse("2020-10-14T17:42:29")).toThrow();
expect(() => date.parse("2020-10-14T17:42:29.123Z")).toThrow();
});
Comment on lines +482 to +498
Copy link
Contributor

Choose a reason for hiding this comment

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

how about invalid dates?

  expect(() => date.parse("2000-01-32")).toThrow();
  expect(() => date.parse("2000-13-01")).toThrow();
  expect(() => date.parse("2001-02-29")).toThrow();


test("time", () => {
const a = z.string().time();
expect(a.isTime).toEqual(true);
});

test("time parsing", () => {
const time = z.string().time();
time.parse("00:00:00");
time.parse("09:52:31");
time.parse("23:59:59.9999999");
expect(() => time.parse("")).toThrow();
expect(() => time.parse("foo")).toThrow();
expect(() => time.parse("00:00:00Z")).toThrow();
expect(() => time.parse("0:00:00")).toThrow();
expect(() => time.parse("00:0:00")).toThrow();
expect(() => time.parse("00:00:0")).toThrow();
expect(() => time.parse("00:00:00.000+00:00")).toThrow();

const time2 = z.string().time({ precision: 2 });
time2.parse("00:00:00.00");
time2.parse("09:52:31.12");
time2.parse("23:59:59.99");
expect(() => time2.parse("")).toThrow();
expect(() => time2.parse("foo")).toThrow();
expect(() => time2.parse("00:00:00")).toThrow();
expect(() => time2.parse("00:00:00.00Z")).toThrow();
expect(() => time2.parse("00:00:00.0")).toThrow();
expect(() => time2.parse("00:00:00.000")).toThrow();
expect(() => time2.parse("00:00:00.00+00:00")).toThrow();

const time3 = z.string().time({ offset: true });
time3.parse("00:00:00Z");
time3.parse("09:52:31Z");
time3.parse("00:00:00+00:00");
time3.parse("00:00:00+0000");
time3.parse("00:00:00.000Z");
time3.parse("00:00:00.000+00:00");
time3.parse("00:00:00.000+0000");
expect(() => time3.parse("")).toThrow();
expect(() => time3.parse("foo")).toThrow();
expect(() => time3.parse("00:00:00")).toThrow();
expect(() => time3.parse("00:00:00.000")).toThrow();

const time4 = z.string().time({ offset: true, precision: 0 });
time4.parse("00:00:00Z");
time4.parse("09:52:31Z");
time4.parse("00:00:00+00:00");
time4.parse("00:00:00+0000");
expect(() => time4.parse("")).toThrow();
expect(() => time4.parse("foo")).toThrow();
expect(() => time4.parse("00:00:00.0")).toThrow();
expect(() => time4.parse("00:00:00.000")).toThrow();
expect(() => time4.parse("00:00:00.000+00:00")).toThrow();
});

test("IP validation", () => {
const ip = z.string().ip();
expect(ip.safeParse("122.122.122.122").success).toBe(true);
Expand Down
Loading
Loading