Skip to content

Commit f1dfabb

Browse files
colinhacksColin McDonnell
and
Colin McDonnell
authored
3.19.0 (#1383)
* Add locales, fix abstract classes in instanceof, fix ZodFunction constructor, remove circular import * Add z.NEVER * Remove circular import * Fix #1300 * Add runtime check for z.tuple, closes #1291 * Document .rest * Stop implicitly adding rest arg to function schemas * Revert to simple constructor * 3.19.0 * Update readme * Update tsversion Co-authored-by: Colin McDonnell <[email protected]>
1 parent e75f348 commit f1dfabb

25 files changed

+541
-346
lines changed

README.md

+35-21
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,7 @@ There are a growing number of tools that are built atop or support Zod natively!
330330
- [`remix-domains`](https://github.com/SeasonedSoftware/remix-domains/): Improves end-to-end type safety in [Remix](https://remix.run/) by leveraging Zod to parse the framework's inputs such as FormData, URLSearchParams, etc.
331331
- [`@zodios/core`](https://github.com/ecyrbe/zodios): A typescript API client with runtime and compile time validation backed by axios and zod.
332332
- [`@runtyping/zod`](https://github.com/johngeorgewright/runtyping/tree/master/packages/zod): Generate zod from static types & JSON schema.
333+
- [`slonik`](https://github.com/gajus/slonik/tree/gajus/add-zod-validation-backwards-compatible#runtime-validation-and-static-type-inference): Node.js Postgres client with strong Zod integration
333334

334335
#### Form integrations
335336

@@ -1073,6 +1074,14 @@ type Athlete = z.infer<typeof athleteSchema>;
10731074
// type Athlete = [string, number, { pointsScored: number }]
10741075
```
10751076

1077+
A variadic ("rest") argument can be added with the `.rest` method.
1078+
1079+
```ts
1080+
const variadicTuple = z.tuple([z.string()]).rest(z.number());
1081+
const result = variadicTuple.parse(["hello", 1, 2, 3]);
1082+
// => [string, ...number[]];
1083+
```
1084+
10761085
## Unions
10771086

10781087
Zod includes a built-in `z.union` method for composing "OR" types.
@@ -1683,34 +1692,33 @@ const Strings = z.array(z.string()).superRefine((val, ctx) => {
16831692
});
16841693
```
16851694

1686-
You can add as many issues as you like. If `ctx.addIssue` is NOT called during the execution of the function, validation passes.
1695+
You can add as many issues as you like. If `ctx.addIssue` is _not_ called during the execution of the function, validation passes.
16871696

16881697
Normally refinements always create issues with a `ZodIssueCode.custom` error code, but with `superRefine` you can create any issue of any code. Each issue code is described in detail in the Error Handling guide: [ERROR_HANDLING.md](ERROR_HANDLING.md).
16891698

16901699
#### Abort early
16911700

1692-
By default, parsing will continue even after a refinement check fails. For instance, if you chain together multiple refinements, they will all be executed. However, it may be desirable to _abort early_ to prevent later refinements from being executed. To achieve this, pass the `fatal` flag to `ctx.addIssue`:
1701+
By default, parsing will continue even after a refinement check fails. For instance, if you chain together multiple refinements, they will all be executed. However, it may be desirable to _abort early_ to prevent later refinements from being executed. To achieve this, pass the `fatal` flag to `ctx.addIssue` and return `z.NEVER`.
16931702

16941703
```ts
1695-
const Strings = z
1696-
.number()
1697-
.superRefine((val, ctx) => {
1698-
if (val < 10) {
1699-
ctx.addIssue({
1700-
code: z.ZodIssueCode.custom,
1701-
message: "foo",
1702-
fatal: true,
1703-
});
1704-
}
1705-
})
1706-
.superRefine((val, ctx) => {
1707-
if (val !== " ") {
1708-
ctx.addIssue({
1709-
code: z.ZodIssueCode.custom,
1710-
message: "bar",
1711-
});
1712-
}
1713-
});
1704+
const schema = z.number().superRefine((val, ctx) => {
1705+
if (val < 10) {
1706+
ctx.addIssue({
1707+
code: z.ZodIssueCode.custom,
1708+
message: "should be >= 10",
1709+
fatal: true,
1710+
});
1711+
1712+
return z.NEVER;
1713+
}
1714+
1715+
if (val !== 12) {
1716+
ctx.addIssue({
1717+
code: z.ZodIssueCode.custom,
1718+
message: "should be twelve",
1719+
});
1720+
}
1721+
});
17141722
```
17151723

17161724
### `.transform`
@@ -1749,6 +1757,12 @@ const Strings = z.string().transform((val, ctx) => {
17491757
code: z.ZodIssueCode.custom,
17501758
message: "Not a number",
17511759
});
1760+
1761+
// This is a special symbol you can use to
1762+
// return early from the transform function.
1763+
// It has type `never` so it does not affect the
1764+
// inferred return type.
1765+
return z.NEVER;
17521766
}
17531767
return parsed;
17541768
});

deno/lib/README.md

+39-25
Original file line numberDiff line numberDiff line change
@@ -306,9 +306,9 @@ There are a growing number of tools that are built atop or support Zod natively!
306306
- [`tRPC`](https://github.com/trpc/trpc): Build end-to-end typesafe APIs without GraphQL.
307307
- [`ts-to-zod`](https://github.com/fabien0102/ts-to-zod): Convert TypeScript definitions into Zod schemas.
308308
- [`zod-to-ts`](https://github.com/sachinraja/zod-to-ts): Generate TypeScript definitions from Zod schemas.
309-
- [`@anatine/zod-openapi`](https://github.com/anatine/zod-plugins/tree/main/libs/zod-openapi): Converts a Zod schema to an OpenAPI v3.x `SchemaObject`.
310-
- [`@anatine/zod-mock`](https://github.com/anatine/zod-plugins/tree/main/libs/zod-mock): Generate mock data from a Zod schema. Powered by [faker.js](https://github.com/Marak/Faker.js).
311-
- [`@anatine/zod-nestjs`](https://github.com/anatine/zod-plugins/tree/main/libs/zod-nestjs): Helper methods for using Zod in a NestJS project.
309+
- [`@anatine/zod-openapi`](https://github.com/anatine/zod-plugins/tree/main/packages/zod-openapi): Converts a Zod schema to an OpenAPI v3.x `SchemaObject`.
310+
- [`@anatine/zod-mock`](https://github.com/anatine/zod-plugins/tree/main/packages/zod-mock): Generate mock data from a Zod schema. Powered by [faker.js](https://github.com/Marak/Faker.js).
311+
- [`@anatine/zod-nestjs`](https://github.com/anatine/zod-plugins/tree/main/packages/zod-nestjs): Helper methods for using Zod in a NestJS project.
312312
- [`zod-mocking`](https://github.com/dipasqualew/zod-mocking): Generate mock data from your Zod schemas.
313313
- [`zod-fast-check`](https://github.com/DavidTimms/zod-fast-check): Generate `fast-check` arbitraries from Zod schemas.
314314
- [`zod-endpoints`](https://github.com/flock-community/zod-endpoints): Contract-first strictly typed endpoints with Zod. OpenAPI compatible.
@@ -330,6 +330,7 @@ There are a growing number of tools that are built atop or support Zod natively!
330330
- [`remix-domains`](https://github.com/SeasonedSoftware/remix-domains/): Improves end-to-end type safety in [Remix](https://remix.run/) by leveraging Zod to parse the framework's inputs such as FormData, URLSearchParams, etc.
331331
- [`@zodios/core`](https://github.com/ecyrbe/zodios): A typescript API client with runtime and compile time validation backed by axios and zod.
332332
- [`@runtyping/zod`](https://github.com/johngeorgewright/runtyping/tree/master/packages/zod): Generate zod from static types & JSON schema.
333+
- [`slonik`](https://github.com/gajus/slonik/tree/gajus/add-zod-validation-backwards-compatible#runtime-validation-and-static-type-inference): Node.js Postgres client with strong Zod integration
333334

334335
#### Form integrations
335336

@@ -799,7 +800,7 @@ Dog.shape.age; // => number schema
799800

800801
### `.keyof`
801802

802-
Use `.key` to create a `ZodEnum` schema from the keys of an object schema.
803+
Use `.keyof` to create a `ZodEnum` schema from the keys of an object schema.
803804

804805
```ts
805806
const keySchema = Dog.keyof();
@@ -1073,6 +1074,14 @@ type Athlete = z.infer<typeof athleteSchema>;
10731074
// type Athlete = [string, number, { pointsScored: number }]
10741075
```
10751076

1077+
A variadic ("rest") argument can be added with the `.rest` method.
1078+
1079+
```ts
1080+
const variadicTuple = z.tuple([z.string()]).rest(z.number());
1081+
const result = variadicTuple.parse(["hello", 1, 2, 3]);
1082+
// => [string, ...number[]];
1083+
```
1084+
10761085
## Unions
10771086

10781087
Zod includes a built-in `z.union` method for composing "OR" types.
@@ -1683,34 +1692,33 @@ const Strings = z.array(z.string()).superRefine((val, ctx) => {
16831692
});
16841693
```
16851694

1686-
You can add as many issues as you like. If `ctx.addIssue` is NOT called during the execution of the function, validation passes.
1695+
You can add as many issues as you like. If `ctx.addIssue` is _not_ called during the execution of the function, validation passes.
16871696

16881697
Normally refinements always create issues with a `ZodIssueCode.custom` error code, but with `superRefine` you can create any issue of any code. Each issue code is described in detail in the Error Handling guide: [ERROR_HANDLING.md](ERROR_HANDLING.md).
16891698

16901699
#### Abort early
16911700

1692-
By default, parsing will continue even after a refinement check fails. For instance, if you chain together multiple refinements, they will all be executed. However, it may be desirable to _abort early_ to prevent later refinements from being executed. To achieve this, pass the `fatal` flag to `ctx.addIssue`:
1701+
By default, parsing will continue even after a refinement check fails. For instance, if you chain together multiple refinements, they will all be executed. However, it may be desirable to _abort early_ to prevent later refinements from being executed. To achieve this, pass the `fatal` flag to `ctx.addIssue` and return `z.NEVER`.
16931702

16941703
```ts
1695-
const Strings = z
1696-
.number()
1697-
.superRefine((val, ctx) => {
1698-
if (val < 10) {
1699-
ctx.addIssue({
1700-
code: z.ZodIssueCode.custom,
1701-
message: "foo",
1702-
fatal: true,
1703-
});
1704-
}
1705-
})
1706-
.superRefine((val, ctx) => {
1707-
if (val !== " ") {
1708-
ctx.addIssue({
1709-
code: z.ZodIssueCode.custom,
1710-
message: "bar",
1711-
});
1712-
}
1713-
});
1704+
const schema = z.number().superRefine((val, ctx) => {
1705+
if (val < 10) {
1706+
ctx.addIssue({
1707+
code: z.ZodIssueCode.custom,
1708+
message: "should be >= 10",
1709+
fatal: true,
1710+
});
1711+
1712+
return z.NEVER;
1713+
}
1714+
1715+
if (val !== 12) {
1716+
ctx.addIssue({
1717+
code: z.ZodIssueCode.custom,
1718+
message: "should be twelve",
1719+
});
1720+
}
1721+
});
17141722
```
17151723

17161724
### `.transform`
@@ -1749,6 +1757,12 @@ const Strings = z.string().transform((val, ctx) => {
17491757
code: z.ZodIssueCode.custom,
17501758
message: "Not a number",
17511759
});
1760+
1761+
// This is a special symbol you can use to
1762+
// return early from the transform function.
1763+
// It has type `never` so it does not affect the
1764+
// inferred return type.
1765+
return z.NEVER;
17521766
}
17531767
return parsed;
17541768
});

deno/lib/ZodError.ts

+4-127
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import type { TypeOf, ZodType } from "./index.ts";
2-
import { jsonStringifyReplacer } from "./helpers/parseUtil.ts";
32
import { Primitive } from "./helpers/typeAliases.ts";
43
import { util, ZodParsedType } from "./helpers/util.ts";
54

@@ -251,7 +250,7 @@ export class ZodError<T = any> extends Error {
251250
return this.message;
252251
}
253252
get message() {
254-
return JSON.stringify(this.issues, jsonStringifyReplacer, 2);
253+
return JSON.stringify(this.issues, util.jsonStringifyReplacer, 2);
255254
}
256255

257256
get isEmpty(): boolean {
@@ -299,134 +298,12 @@ export type IssueData = stripPath<ZodIssueOptionalMessage> & {
299298
};
300299
export type MakeErrorData = IssueData;
301300

302-
type ErrorMapCtx = {
301+
export type ErrorMapCtx = {
303302
defaultError: string;
304303
data: any;
305304
};
306305

307-
export type ZodErrorMap = typeof defaultErrorMap;
308-
export const defaultErrorMap = (
306+
export type ZodErrorMap = (
309307
issue: ZodIssueOptionalMessage,
310308
_ctx: ErrorMapCtx
311-
): { message: string } => {
312-
let message: string;
313-
switch (issue.code) {
314-
case ZodIssueCode.invalid_type:
315-
if (issue.received === ZodParsedType.undefined) {
316-
message = "Required";
317-
} else {
318-
message = `Expected ${issue.expected}, received ${issue.received}`;
319-
}
320-
break;
321-
case ZodIssueCode.invalid_literal:
322-
message = `Invalid literal value, expected ${JSON.stringify(
323-
issue.expected,
324-
jsonStringifyReplacer
325-
)}`;
326-
break;
327-
case ZodIssueCode.unrecognized_keys:
328-
message = `Unrecognized key(s) in object: ${util.joinValues(
329-
issue.keys,
330-
", "
331-
)}`;
332-
break;
333-
case ZodIssueCode.invalid_union:
334-
message = `Invalid input`;
335-
break;
336-
case ZodIssueCode.invalid_union_discriminator:
337-
message = `Invalid discriminator value. Expected ${util.joinValues(
338-
issue.options
339-
)}`;
340-
break;
341-
case ZodIssueCode.invalid_enum_value:
342-
message = `Invalid enum value. Expected ${util.joinValues(
343-
issue.options
344-
)}, received '${issue.received}'`;
345-
break;
346-
case ZodIssueCode.invalid_arguments:
347-
message = `Invalid function arguments`;
348-
break;
349-
case ZodIssueCode.invalid_return_type:
350-
message = `Invalid function return type`;
351-
break;
352-
case ZodIssueCode.invalid_date:
353-
message = `Invalid date`;
354-
break;
355-
case ZodIssueCode.invalid_string:
356-
if (typeof issue.validation === "object") {
357-
if ("startsWith" in issue.validation) {
358-
message = `Invalid input: must start with "${issue.validation.startsWith}"`;
359-
} else if ("endsWith" in issue.validation) {
360-
message = `Invalid input: must end with "${issue.validation.endsWith}"`;
361-
} else {
362-
util.assertNever(issue.validation);
363-
}
364-
} else if (issue.validation !== "regex") {
365-
message = `Invalid ${issue.validation}`;
366-
} else {
367-
message = "Invalid";
368-
}
369-
break;
370-
case ZodIssueCode.too_small:
371-
if (issue.type === "array")
372-
message = `Array must contain ${
373-
issue.inclusive ? `at least` : `more than`
374-
} ${issue.minimum} element(s)`;
375-
else if (issue.type === "string")
376-
message = `String must contain ${
377-
issue.inclusive ? `at least` : `over`
378-
} ${issue.minimum} character(s)`;
379-
else if (issue.type === "number")
380-
message = `Number must be greater than ${
381-
issue.inclusive ? `or equal to ` : ``
382-
}${issue.minimum}`;
383-
else if (issue.type === "date")
384-
message = `Date must be greater than ${
385-
issue.inclusive ? `or equal to ` : ``
386-
}${new Date(issue.minimum)}`;
387-
else message = "Invalid input";
388-
break;
389-
case ZodIssueCode.too_big:
390-
if (issue.type === "array")
391-
message = `Array must contain ${
392-
issue.inclusive ? `at most` : `less than`
393-
} ${issue.maximum} element(s)`;
394-
else if (issue.type === "string")
395-
message = `String must contain ${
396-
issue.inclusive ? `at most` : `under`
397-
} ${issue.maximum} character(s)`;
398-
else if (issue.type === "number")
399-
message = `Number must be less than ${
400-
issue.inclusive ? `or equal to ` : ``
401-
}${issue.maximum}`;
402-
else if (issue.type === "date")
403-
message = `Date must be smaller than ${
404-
issue.inclusive ? `or equal to ` : ``
405-
}${new Date(issue.maximum)}`;
406-
else message = "Invalid input";
407-
break;
408-
case ZodIssueCode.custom:
409-
message = `Invalid input`;
410-
break;
411-
case ZodIssueCode.invalid_intersection_types:
412-
message = `Intersection results could not be merged`;
413-
break;
414-
case ZodIssueCode.not_multiple_of:
415-
message = `Number must be a multiple of ${issue.multipleOf}`;
416-
break;
417-
default:
418-
message = _ctx.defaultError;
419-
util.assertNever(issue);
420-
}
421-
return { message };
422-
};
423-
424-
let overrideErrorMap = defaultErrorMap;
425-
426-
export function setErrorMap(map: ZodErrorMap) {
427-
overrideErrorMap = map;
428-
}
429-
430-
export function getErrorMap() {
431-
return overrideErrorMap;
432-
}
309+
) => { message: string };

deno/lib/__tests__/function.test.ts

+2
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@ test("output validation error", () => {
110110
expect(checker).toThrow();
111111
});
112112

113+
z.function(z.tuple([z.string()])).args()._def.args;
114+
113115
test("special function error codes", () => {
114116
const checker = z
115117
.function(z.tuple([z.string()]), z.boolean())

deno/lib/__tests__/instanceof.test.ts

+7
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,20 @@ import * as z from "../index.ts";
88
test("instanceof", async () => {
99
class Test {}
1010
class Subtest extends Test {}
11+
abstract class AbstractBar {
12+
constructor(public val: string) {}
13+
}
14+
class Bar extends AbstractBar {}
1115

1216
const TestSchema = z.instanceof(Test);
1317
const SubtestSchema = z.instanceof(Subtest);
18+
const BarSchema = z.instanceof(Bar);
1419

1520
TestSchema.parse(new Test());
1621
TestSchema.parse(new Subtest());
1722
SubtestSchema.parse(new Subtest());
23+
const bar = BarSchema.parse(new Bar("asdf"));
24+
expect(bar.val).toEqual("asdf");
1825

1926
await expect(() => SubtestSchema.parse(new Test())).toThrow(
2027
/Input not instance of Subtest/

0 commit comments

Comments
 (0)