Cleanup up custom error handling. Wrote ERROR_HANDLING.md.

This commit is contained in:
Colin McDonnell 2020-07-08 15:22:46 -07:00
parent a4a79728ea
commit 3fe301dd22
21 changed files with 924 additions and 257 deletions

0
.eslintignore Normal file
View File

211
ERROR_HANDLING.md Normal file
View File

@ -0,0 +1,211 @@
# Error Handling in Zod
This guide explains Zod's internal error handling system, and the various ways you can customize it for your purposes.
## ZodError
All validation errors thrown by Zod are instances of `ZodError`. ZodError is a subclass of `Error`; if you want to place around with this class, you can create an instance like so:
```ts
import * as z from 'zod';
const myError = new z.ZodError([]);
```
All parsing/validation errors thrown by Zod are instances of `ZodError`. Detailed information about the validation issues is available in the `errors` property.
## A demonstrative example
This array represents all errors Zod encounters when attempting to parse a value.
```ts
const person = z.object({
names: z.array(z.string()).nonempty(), // at least 1 name
address: z.object({
line1: z.string(),
zipCode: z.number().min(10000), // American 5-digit code
}),
});
try {
person.parse({
names: ['Dave', 12],
address: {
line1: '123 Maple Ave',
zipCode: 123,
extra: 'other stuff',
},
});
} catch (err) {
if (err instanceof z.ZodError) {
// ZodSuberror[]
console.log(err.errors);
} else {
// this should never happen
throw err;
}
}
```
Here are the errors that will be thrown:
```ts
// ZodSuberror[]
[
{
code: 'invalid_type',
expected: 'string',
received: 'number',
path: ['names', 1],
message: 'Invalid input: expected string, received number',
},
{
code: 'unrecognized_keys',
keys: ['extra'],
path: ['address'],
message: "Unrecognized key(s) in object: 'extra'",
},
{
code: 'too_small',
minimum: 10000,
type: 'number',
inclusive: true,
path: ['address', 'zipCode'],
message: 'Value should be greater than or equal to 10000',
},
];
```
As you can see three different issues were identified. Every ZodSuberror has a `code` property and additional metadata about the validation failure. For instance the `unrecognized_keys` error provides a list of the unrecognized keys detected in teh input.
### ZodSuberror
`ZodSuberror` is _not_ a class. It is a [discriminated union](https://www.typescriptlang.org/docs/handbook/advanced-types.html#discriminated-unions).
The link above the the best way to learn about the concept. Discriminated unions are an ideal way to represent a data structures that may be one of many possible variants.
Every ZodSuberror has these fields:
| field | type | details |
| --------- | --------------------- | ----------------------------------------------------------------------------------------------------- |
| `code` | `z.ZodErrorCode` | You can access all possible values using the `z.ZodErrorCode` enum e.g. `z.ZodErrorCode.invalid_type` |
| `path` | `(string | number)[]` | `['addresses', 0, 'line1']` |
| `message` | `string` | `Invalid type. Expected string, received number.` |
**However** depending on the error code, there may be additional properties as well. Here is a full breakdown of the additional fields by error code:
### ZodSuberror
| code | additional fields |
| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| ZodErrorCode.invalid_type | `expected: ZodParsedType` <br> `received: ZodParsedType` <br><br>Jump to [this section](#parsedtype) for a breakdownthe possible values of ZodParsedType. |
| ZodErrorCode.nonempty_array_is_empty | _no additional properties_ |
| ZodErrorCode.unrecognized_keys | `keys: string[]`<br>The list of unrecognized keys<br> |
| ZodErrorCode.invalid_union | `unionErrors: ZodError[]` <br> The errors thrown by each elements of the union. |
| <!-- | ZodErrorCode.invalid_tuple_length | `expected: number` <br>The expected length.<br> <br>`received: number`<br> The actual length.<br> | --> |
| ZodErrorCode.invalid_literal_value | `expected: string | number | boolean` <br> The literal value. |
| ZodErrorCode.invalid_enum_value | `options: string[]` <br> The set of acceptable string values for this enum. |
| ZodErrorCode.invalid_arguments | `argumentsError: ZodError` <br> This is a special error code only thrown by a wrapped function returned by `ZodFunction.implement()`. The `argumentsError` property is another ZodError containing the validation error details. |
| ZodErrorCode.invalid_return_type | `returnTypeError: ZodError` <br> This is a special error code only thrown by a wrapped function returned by `ZodFunction.implement()`. The `returnTypeError` property is another ZodError containing the validation error details. |
| ZodErrorCode.invalid_date | _no additional properties_ |
| ZodErrorCode.invalid_string | `validation: "url" | "email" | "uuid"`<br> Which built-in string validator failed |
| ZodErrorCode.too_small | `type: "string" | "number" | "array"` <br>The type of the data failing validation<br><br> `minimum: number` <br>The expected length/value.<br><br>`inclusive: boolean`<br>Whether the minimum is included in the range of acceptable values.<br> |
| ZodErrorCode.too_big | `type: "string" | "number" | "array"` <br>The type of the data failing validation<br><br> `maximum: number` <br>The expected length/value.<br><br>`inclusive: boolean`<br>Whether the minimum is included in the range of acceptable values.<br> |
| ZodErrorCode.custom_error | `params: { [k: string]: any }` <br> This is the error code throw by **all custom refinements**. You are able to pass in a `params` object here that is available in your custom error maps (see [ZodErrorMap](#Customizing-errors-with-ZodErrorMap) below for details on error maps) |
### ZodParsedType
This is an enum used byn Zod internally to represent the type of a parsed value. The possible values are:
- `string`
- `nan`
- `number`
- `integer`
- `boolean`
- `date`
- `bigint`
- `symbol`
- `function`
- `undefined`
- `null`
- `array`
- `object`
- `unknown`
- `promise`
- `void`
### Customizing errors with ZodErrorMap
You can customize **all** error messages produced by Zod by providing a custom instance of ZodErrorMap to `.parse()`. Internally, Zod uses a [default error map](https://github.com/vriad/zod/blob/master/defaultErrorMap.ts) to produce all error messages.
`ZodErrorMap` is a special function. It accepts two arguments: `error` and `ctx`. The return type is `{ message: string }`. Essentially the error map accepts some information about the validation that is failing and returns an appropriate error message.
- `error: Omit<ZodSuberror, "message">`
As mentioned above, ZodSuberror is a discriminated union.
- `ctx: { defaultError: string; data: any}`
- `ctx.default` is the error message generated by the default error map. If you only want to override the message for a single type of error, you can do that. Just return `defaultError` for everything
- `ctx.data` contains the data that was passed into `.parse`. You can use this to customize the error message.
### A working example
Let's look at a practical example of of customized error map:
```ts
import * as z from 'zod';
const errorMap: z.ZodErrorMap = (error, ctx) => {
/*
If error.message is set, that means the user is trying to
override the error message. This is how method-specific
error overrides work, like this:
z.string().min(5, { message: "TOO SMALL 🤬" })
It is a best practice to return `error.message` if it is set.
*/
if (error.message) return { message: error.message };
/*
This is where you override the various error codes
*/
switch (error.code) {
case z.ZodErrorCode.invalid_type:
if (error.expected === 'string') {
return { message: `This ain't a string!` };
}
break;
case z.ZodErrorCode.custom_error:
// produce a custom message using error.params
// error.params won't be set unless you passed
// a `params` arguments into a custom validator
const params = error.params || {};
if (params.myField) {
return { message: `Bad input: ${params.myField}` };
}
break;
}
// fall back to default message!
return { message: ctx.defaultError };
};
z.string().parse(12, { errorMap });
/* throws:
ZodError {
errors: [{
code: "invalid_type",
path: [],
message: "This ain't a string!",
expected: "string",
received: "number",
}]
}
*/
```

1
FUNDING.yml Normal file
View File

@ -0,0 +1 @@
github: vriad

103
README.md
View File

@ -73,15 +73,24 @@ npm install --save zod
yarn add zod
```
#### TypeScript versions
#### TypeScript requirements
⚠ Zod 2.0.x requires TypeScript 4.0+
> You must use strict mode for Zod to correctly infer the types of your schemas! Add `"strict": true` inside "compilerOptions" in your `tsconfig.json`.
1. Zod 1.x requires TypeScript 3.2+
2. You must configure your project to use TypeScript's **strict mode**. Otherwise Zod can't correctly infer the types of your schemas!
```ts
// tsconfig.json
{
// ...
"compilerOptions": {
// ...
"strict": true
}
}
```
# Usage
Zod is a validation library designed for optimal developer experience. It's a TypeScript-first schema declaration library with rigorous (and correct!) inferred types, incredible developer experience, and a few killer features missing from the existing libraries.
Zod is a validation library designed for optimal developer experience. It's a TypeScript-first schema declaration library with rigorous inferred types, incredible developer experience, and a few killer features missing from the existing libraries.
<!-- - It infers the statically types of your schemas
- Eliminates the need to keep static types and runtime validators in sync by hand
@ -112,6 +121,7 @@ z.date();
// empty types
z.undefined();
z.null();
z.void();
// catch-all types
z.any();
@ -223,18 +233,20 @@ z.string().max(5);
z.string().length(5);
z.string().email();
z.string().url();
z.string().uuid();
```
### Custom error messages
Like `.refine`, the final argument accepts a custom error message.
Like `.refine`, The final (optional) argument is an object that lets you provide a custom error in the `message` field.
```ts
z.string().min(5, 'Must be 5 or more characters long');
z.string().max(5, 'Must be 5 or fewer characters long');
z.string().length(5, 'Must be exactly 5 characters long');
z.string().email('Invalid email address.');
z.string().url('Invalid url');
z.string().min(5, { message: 'Must be 5 or more characters long' });
z.string().max(5, { message: 'Must be 5 or fewer characters long' });
z.string().length(5, { message: 'Must be exactly 5 characters long' });
z.string().email({ message: 'Invalid email address.' });
z.string().url({ message: 'Invalid url' });
z.string().url({ message: 'Invalid url' });
```
> To see the email and url regexes, check out [this file](https://github.com/vriad/zod/blob/master/src/types/string.ts). To use a more advanced method, use a custom refinement.
@ -243,11 +255,11 @@ z.string().url('Invalid url');
There are a handful of number-specific validations.
All of these validations allow you to _optionally_ specify a custom error message as a final `string` argument.
The final (optional) argument is a params object that lets you provide a custom error in the `message` field.
```ts
z.number().min(5);
z.number().max(5);
z.number().max(5, { message: 'this👏is👏too👏big' });
z.number().int(); // value must be an integer
@ -928,6 +940,8 @@ You can create a function schema with `z.function(args, returnType)` which accep
- `args: ZodTuple` The first argument is a tuple (created with `z.tuple([...])` and defines the schema of the arguments to your function. If the function doesn't accept arguments, you can pass an empty tuple (`z.tuple([])`).
- `returnType: any Zod schema` The second argument is the function's return type. This can be any Zod schema.
> You can the special `z.void()` option if your function doesn't return anything. This will let Zod properly infer the type of void-returning functions. (Void-returning function can actually return either undefined or null.)
```ts
const args = z.tuple([z.string()]);
@ -1068,68 +1082,7 @@ User.omit({ outer: { inner: { prop2: true } } }); // { outer: { prop1: string, i
## Errors
Zod includes a custom `Error` subclass called `ZodError`. All validation errors thrown by Zod are instances of `ZodError`.
A `ZodError` instance has an `errors` property of type
```ts
// ZodError#errors
{
path: (string | number)[],
message: string
}[]
```
This array represents all errors Zod encounters when attempting to parse a value.
```ts
const person = z.object({
name: {
first: z.string(),
last: z.string(),
},
age: z.number(),
address: z.array(z.string()),
});
try {
person.parse({
name: { first: 'Dave', last: 42 },
age: 'threeve',
address: ['123 Maple Street', {}],
});
} catch (err) {
if (err instanceof ZodError) {
console.log(JSON.stringify(err.errors));
/*
[
{
"path": [ "name", "last" ],
"message": "Non-string type: number"
},
{
"path": [ "age" ],
"message": "Non-number type: string"
},
{
"path": [ "address", 1 ],
"message": "Non-string type: object"
}
]
*/
// err.message returns a formatted error message
console.log(err.message);
/*
`name.last`: Non-string type: number
`age`: Non-number type: string
`address.1`: Non-string type: object
*/
} else {
// should never happen
}
}
```
There is a dedicated guide on Zod's error handling system here: [ERROR_HANDLING.md](https://github.com/vriad/zod/blob/master/ERROR_HANDLING.md)
# Comparison

View File

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="116" height="20"><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="116" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="63" height="20" fill="#555"/><rect x="63" width="53" height="20" fill="#dfb317"/><rect width="116" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text x="325" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="530">Coverage</text><text x="325" y="140" transform="scale(.1)" textLength="530">Coverage</text><text x="885" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="430">89.71%</text><text x="885" y="140" transform="scale(.1)" textLength="430">89.71%</text></g></svg>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="116" height="20"><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="116" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="63" height="20" fill="#555"/><rect x="63" width="53" height="20" fill="#dfb317"/><rect width="116" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text x="325" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="530">Coverage</text><text x="325" y="140" transform="scale(.1)" textLength="530">Coverage</text><text x="885" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="430">89.63%</text><text x="885" y="140" transform="scale(.1)" textLength="430">89.63%</text></g></svg>

Before

Width:  |  Height:  |  Size: 1015 B

After

Width:  |  Height:  |  Size: 1015 B

View File

@ -1,6 +1,6 @@
{
"name": "zod",
"version": "2.0.0-alpha.7",
"version": "1.8.0-beta.1",
"description": "TypeScript-first schema declaration and validation library with static type inference",
"main": "./lib/src/index.js",
"types": "./lib/src/index.d.ts",
@ -53,6 +53,6 @@
"ts-jest": "^25.2.1",
"tslint": "^6.1.0",
"tslint-config-prettier": "^1.18.0",
"typescript": "3.9"
"typescript": "3.2"
}
}

View File

@ -1,4 +1,4 @@
import { ParsedType } from './parser';
import { ZodParsedType } from './parser';
import { util } from './helpers/util';
export const ZodErrorCode = util.arrayToEnum([
@ -6,14 +6,20 @@ export const ZodErrorCode = util.arrayToEnum([
'nonempty_array_is_empty',
'custom_error',
'invalid_union',
'invalid_array_length',
'array_empty',
// 'invalid_tuple_length',
'invalid_literal_value',
'invalid_enum_value',
'unrecognized_keys',
'invalid_arguments',
'invalid_return_type',
'invalid_date',
// 'too_short',
// 'too_long',
'invalid_string',
// 'invalid_url',
// 'invalid_uuid',
'too_small',
'too_big',
]);
export type ZodErrorCode = keyof typeof ZodErrorCode;
@ -22,13 +28,12 @@ export type ZodSuberrorBase = {
path: (string | number)[];
code: ZodErrorCode;
message?: string;
suberrors?: ZodError[];
};
interface InvalidTypeError extends ZodSuberrorBase {
code: typeof ZodErrorCode.invalid_type;
expected: ParsedType;
received: ParsedType;
expected: ZodParsedType;
received: ZodParsedType;
}
interface NonEmptyArrayIsEmptyError extends ZodSuberrorBase {
@ -42,13 +47,14 @@ interface UnrecognizedKeysError extends ZodSuberrorBase {
interface InvalidUnionError extends ZodSuberrorBase {
code: typeof ZodErrorCode.invalid_union;
unionErrors: ZodError[];
}
interface InvalidArrayLengthError extends ZodSuberrorBase {
code: typeof ZodErrorCode.invalid_array_length;
expected: number;
received: number;
}
// interface InvalidArrayLengthError extends ZodSuberrorBase {
// code: typeof ZodErrorCode.invalid_tuple_length;
// expected: number;
// received: number;
// }
interface InvalidLiteralValueError extends ZodSuberrorBase {
code: typeof ZodErrorCode.invalid_literal_value;
@ -62,16 +68,57 @@ interface InvalidEnumValueError extends ZodSuberrorBase {
interface InvalidArgumentsError extends ZodSuberrorBase {
code: typeof ZodErrorCode.invalid_arguments;
argumentsError: ZodError;
}
interface InvalidReturnTypeError extends ZodSuberrorBase {
code: typeof ZodErrorCode.invalid_return_type;
returnTypeError: ZodError;
}
interface InvalidDateError extends ZodSuberrorBase {
code: typeof ZodErrorCode.invalid_date;
}
// interface TooShortError extends ZodSuberrorBase {
// code: typeof ZodErrorCode.too_small;
// minimum: number;
// }
// interface TooLongError extends ZodSuberrorBase {
// code: typeof ZodErrorCode.too_big;
// maximum: number;
// }
interface InvalidStringError extends ZodSuberrorBase {
code: typeof ZodErrorCode.invalid_string;
validation: 'email' | 'url' | 'uuid';
}
// interface InvalidUrlError extends ZodSuberrorBase {
// code: typeof ZodErrorCode.invalid_url;
// validation: | 'url';
// }
// interface InvalidUuidError extends ZodSuberrorBase {
// code: typeof ZodErrorCode.invalid_uuid;
// validation: | 'uuid';
// }
interface TooSmallError extends ZodSuberrorBase {
code: typeof ZodErrorCode.too_small;
minimum: number;
inclusive: boolean;
type: 'array' | 'string' | 'number';
}
interface TooBigError extends ZodSuberrorBase {
code: typeof ZodErrorCode.too_big;
maximum: number;
inclusive: boolean;
type: 'array' | 'string' | 'number';
}
interface CustomError extends ZodSuberrorBase {
code: typeof ZodErrorCode.custom_error;
params?: { [k: string]: any };
@ -82,12 +129,19 @@ export type ZodSuberrorOptionalMessage =
| NonEmptyArrayIsEmptyError
| UnrecognizedKeysError
| InvalidUnionError
| InvalidArrayLengthError
// | InvalidArrayLengthError
| InvalidLiteralValueError
| InvalidEnumValueError
| InvalidArgumentsError
| InvalidReturnTypeError
| InvalidDateError
// | TooShortError
// | TooLongError
| InvalidStringError // | InvalidEmailError
// | InvalidUrlError
// | InvalidUuidError
| TooSmallError
| TooBigError
| CustomError;
export type ZodSuberror = ZodSuberrorOptionalMessage & { message: string };

View File

@ -1,13 +1,13 @@
import * as z from '..';
import { ZodError, ZodErrorCode } from '../ZodError';
import { ParsedType } from '../parser';
import { ZodParsedType } from '../parser';
test('error creation', () => {
const err1 = ZodError.create([]);
err1.addError({
code: ZodErrorCode.invalid_type,
expected: ParsedType.object,
received: ParsedType.string,
expected: ZodParsedType.object,
received: ZodParsedType.string,
path: [],
message: '',
});
@ -22,25 +22,49 @@ test('error creation', () => {
err3.message;
});
test('custom errormap', () => {
const errorMap: z.ZodErrorMap = (error, ctx) => {
if (error.code === ZodErrorCode.invalid_type) {
if (error.expected === 'string') {
return "This ain't no string!";
}
const errorMap: z.ZodErrorMap = (error, ctx) => {
if (error.code === ZodErrorCode.invalid_type) {
if (error.expected === 'string') {
return { message: 'bad type!' };
}
if (error.code === ZodErrorCode.custom_error) {
return JSON.stringify(error.params, null, 2);
}
return ctx.defaultError;
};
errorMap;
}
if (error.code === ZodErrorCode.custom_error) {
return { message: `less-than-${(error.params || {}).minimum}` };
}
return { message: ctx.defaultError };
};
test('type error with custom error map', () => {
try {
z.string().parse('asdf', { errorMap });
} catch (err) {
const zerr: z.ZodError = err;
expect(zerr.errors[0].code).toEqual(z.ZodErrorCode.invalid_type);
expect(zerr.errors[0].message).toEqual(`bad type!`);
}
});
test('refinement fail with params', () => {
try {
z.number()
.refinement({
check: val => val >= 3,
params: { minimum: 3 },
})
.parse(2, { errorMap });
} catch (err) {
const zerr: z.ZodError = err;
expect(zerr.errors[0].code).toEqual(z.ZodErrorCode.custom_error);
expect(zerr.errors[0].message).toEqual(`less-than-3`);
}
});
test('custom error with custom errormap', () => {
try {
z.string()
.refinement({
check: val => val.length > 12,
// params: { test: 15 },
params: { minimum: 13 },
message: 'override',
})
.parse('asdf', { errorMap });
@ -50,4 +74,69 @@ test('custom errormap', () => {
}
});
test('default error message', () => {
try {
z.number()
.refine(x => x > 3)
.parse(2);
} catch (err) {
const zerr: z.ZodError = err;
expect(zerr.errors.length).toEqual(1);
expect(zerr.errors[0].message).toEqual('Invalid value.');
}
});
test('override error in refine', () => {
try {
z.number()
.refine(x => x > 3, 'override')
.parse(2);
} catch (err) {
const zerr: z.ZodError = err;
expect(zerr.errors.length).toEqual(1);
expect(zerr.errors[0].message).toEqual('override');
}
});
test('override error in refinement', () => {
try {
z.number()
.refinement({
check: x => x > 3,
message: 'override',
})
.parse(2);
} catch (err) {
const zerr: z.ZodError = err;
expect(zerr.errors.length).toEqual(1);
expect(zerr.errors[0].message).toEqual('override');
}
});
test('array minimum', () => {
try {
z.array(z.string())
.min(3, 'tooshort')
.parse(['asdf', 'qwer']);
} catch (err) {
const zerr: ZodError = err;
expect(zerr.errors[0].code).toEqual(ZodErrorCode.too_small);
expect(zerr.errors[0].message).toEqual('tooshort');
}
try {
z.array(z.string())
.min(3)
.parse(['asdf', 'qwer']);
} catch (err) {
const zerr: ZodError = err;
expect(zerr.errors[0].code).toEqual(ZodErrorCode.too_small);
expect(zerr.errors[0].message).toEqual(`Should have at least 3 items`);
}
});
// implement test for semi-smart union logic that checks for type error on either left or right
test('union smart errors', () => {
try {
z.union([z.string(), z.number().int()]).parse(3.2);
} catch (err) {}
});

View File

@ -116,15 +116,18 @@ test('special function error codes', () => {
checker('12' as any);
} catch (err) {
const zerr: z.ZodError = err;
expect(zerr.errors[0].code).toEqual(z.ZodErrorCode.invalid_return_type);
expect(zerr.errors[0].suberrors!.length).toEqual(1);
const first = zerr.errors[0];
if (first.code !== z.ZodErrorCode.invalid_return_type) throw new Error();
expect(first.returnTypeError).toBeInstanceOf(z.ZodError);
}
try {
checker(12 as any);
} catch (err) {
const zerr: z.ZodError = err;
expect(zerr.errors[0].code).toEqual(z.ZodErrorCode.invalid_arguments);
expect(zerr.errors[0].suberrors!.length).toEqual(1);
const first = zerr.errors[0];
if (first.code !== z.ZodErrorCode.invalid_arguments) throw new Error();
expect(first.argumentsError).toBeInstanceOf(z.ZodError);
}
});

View File

@ -0,0 +1,91 @@
import * as z from '..';
test('array min', () => {
z.array(z.string())
.min(4)
.parseAsync([])
.catch(err => {
expect(err.errors[0].message).toEqual('Should have at least 4 items');
});
});
test('array max', () => {
z.array(z.string())
.max(2)
.parseAsync(['asdf', 'asdf', 'asdf'])
.catch(err => {
expect(err.errors[0].message).toEqual('Should have at most 2 items');
});
});
test('string min', () => {
z.string()
.min(4)
.parseAsync('asd')
.catch(err => {
expect(err.errors[0].message).toEqual('Should be at least 4 characters');
});
});
test('string max', () => {
z.string()
.max(4)
.parseAsync('aasdfsdfsd')
.catch(err => {
expect(err.errors[0].message).toEqual('Should be at most 4 characters long');
});
});
test('number min', () => {
z.number()
.min(3)
.parseAsync(2)
.catch(err => {
expect(err.errors[0].message).toEqual('Value should be greater than or equal to 3');
});
});
test('number max', () => {
z.number()
.max(3)
.parseAsync(4)
.catch(err => {
expect(err.errors[0].message).toEqual('Value should be less than or equal to 3');
});
});
test('number nonnegative', () => {
z.number()
.nonnegative()
.parseAsync(-1)
.catch(err => {
expect(err.errors[0].message).toEqual('Value should be greater than or equal to 0');
});
});
test('number nonpositive', () => {
z.number()
.nonpositive()
.parseAsync(1)
.catch(err => {
expect(err.errors[0].message).toEqual('Value should be less than or equal to 0');
});
});
test('number negative', () => {
z.number()
.negative()
.parseAsync(1)
.catch(err => {
expect(err.errors[0].message).toEqual('Value should be less than 0');
});
});
test('number positive', () => {
z.number()
.positive()
.parseAsync(-1)
.catch(err => {
expect(err.errors[0].message).toEqual('Value should be greater than 0');
});
});

90
src/defaultErrorMap.ts Normal file
View File

@ -0,0 +1,90 @@
import { ZodErrorCode, ZodSuberrorOptionalMessage } from './ZodError';
import { util } from './helpers/util';
type ErrorMapCtx = {
// path: (string | number)[];
// details: any;
defaultError: string;
data: any;
// metadata: object;
};
export type ZodErrorMap = typeof defaultErrorMap;
export const defaultErrorMap = (error: ZodSuberrorOptionalMessage, _ctx: ErrorMapCtx): { message: string } => {
let message: string;
switch (error.code) {
case ZodErrorCode.invalid_type:
message = `Invalid input: expected ${error.expected}, received ${error.received}`;
break;
case ZodErrorCode.nonempty_array_is_empty:
message = `List must contain at least one item`;
break;
case ZodErrorCode.unrecognized_keys:
message = `Unrecognized key(s) in object: ${error.keys.map(k => `'${k}'`).join(', ')}`;
break;
case ZodErrorCode.invalid_union:
message = `Invalid input`;
break;
// case ZodErrorCode.invalid_tuple_length:
// message = `Expected list of ${error.expected} items, received ${error.received} items`;
// break;
case ZodErrorCode.invalid_literal_value:
message = `Input must be "${error.expected}"`;
break;
case ZodErrorCode.invalid_enum_value:
message = `Input must be one of these values: ${error.options.join(', ')}`;
break;
case ZodErrorCode.invalid_arguments:
message = `Invalid function arguments`;
break;
case ZodErrorCode.invalid_return_type:
message = `Invalid function return type`;
break;
case ZodErrorCode.invalid_date:
message = `Invalid date`;
break;
// case ZodErrorCode.too_small:
// const tooShortNoun = _ctx.data === 'string' ? 'characters' : 'items';
// message = `Too short, should be at least ${error.minimum} ${tooShortNoun}`;
// break;
// case ZodErrorCode.too_big:
// const tooLongNoun = _ctx.data === 'string' ? 'characters' : 'items';
// message = `Too short, should be at most ${error.maximum} ${tooLongNoun}`;
// break;
case ZodErrorCode.invalid_string:
message = `Invalid ${error.validation}`;
break;
// case ZodErrorCode.invalid_url:
// message = 'Invalid URL.';
// break;
// case ZodErrorCode.invalid_uuid:
// message = 'Invalid UUID.';
// break;
case ZodErrorCode.too_small:
if (error.type === 'array')
message = `Should have ${error.inclusive ? `at least` : `more than`} ${error.minimum} items`;
else if (error.type === 'string')
message = `Should be ${error.inclusive ? `at least` : `over`} ${error.minimum} characters`;
else if (error.type === 'number')
message = `Value should be greater than ${error.inclusive ? `or equal to ` : ``}${error.minimum}`;
else message = 'Invalid input';
break;
case ZodErrorCode.too_big:
if (error.type === 'array')
message = `Should have ${error.inclusive ? `at most` : `less than`} ${error.maximum} items`;
else if (error.type === 'string')
message = `Should be ${error.inclusive ? `at most` : `under`} ${error.maximum} characters long`;
else if (error.type === 'number')
message = `Value should be less than ${error.inclusive ? `or equal to ` : ``}${error.maximum}`;
else message = 'Invalid input';
break;
case ZodErrorCode.custom_error:
message = `Invalid input.`;
break;
default:
message = `Invalid input.`;
util.assertNever(error);
}
return { message };
// return `Invalid input.`;
};

View File

@ -1,55 +0,0 @@
import { ZodErrorCode, ZodSuberrorOptionalMessage } from './ZodError';
import { util } from './helpers/util';
type ErrorMapCtx = {
// path: (string | number)[];
// details: any;
defaultError: string;
data: any;
metadata: object;
};
export type ZodErrorMap = typeof defaultErrorMap;
export const defaultErrorMap = (error: ZodSuberrorOptionalMessage, _ctx: ErrorMapCtx): string => {
let message: string;
switch (error.code) {
case ZodErrorCode.invalid_type:
message = `Invalid input: expected ${error.expected}, received ${error.received}`;
break;
case ZodErrorCode.nonempty_array_is_empty:
message = `List must contain at least one item`;
break;
case ZodErrorCode.unrecognized_keys:
message = `Unrecognized key(s) in object: ${error.keys.map(k => `'${k}'`).join(', ')}`;
break;
case ZodErrorCode.invalid_union:
message = `Invalid input`;
break;
case ZodErrorCode.invalid_array_length:
message = `Expected list of ${error.expected} items, received ${error.received} items`;
break;
case ZodErrorCode.invalid_literal_value:
message = `Input must be "${error.expected}"`;
break;
case ZodErrorCode.invalid_enum_value:
message = `Input must be one of these values: ${error.options.join(', ')}`;
break;
case ZodErrorCode.invalid_arguments:
message = `Invalid function arguments`;
break;
case ZodErrorCode.invalid_return_type:
message = `Invalid function return type`;
break;
case ZodErrorCode.invalid_date:
message = `Invalid date`;
break;
case ZodErrorCode.custom_error:
message = `Invalid input.`;
break;
default:
message = `Invalid input.`;
util.assertNever(error);
}
return message;
// return `Invalid input.`;
};

4
src/helpers/errorUtil.ts Normal file
View File

@ -0,0 +1,4 @@
export namespace errorUtil {
export type ErrMessage = string | { message?: string };
export const errToObj = (message?: ErrMessage) => (typeof message === 'string' ? { message } : message || {});
}

View File

@ -23,7 +23,9 @@ import { ZodEnum, ZodEnumDef } from './types/enum';
import { ZodPromise, ZodPromiseDef } from './types/promise';
import { TypeOf, ZodType, ZodTypeAny, ZodTypeDef, ZodTypes } from './types/base';
import { ZodError, ZodErrorCode } from './ZodError';
import { ZodErrorMap } from './errorMap';
import { ZodParsedType } from './parser';
import { ZodErrorMap } from './defaultErrorMap';
import { ZodCodeGenerator } from './codegen';
@ -170,6 +172,7 @@ export {
ZodError,
ZodErrorMap,
ZodErrorCode,
ZodParsedType,
ZodCodeGenerator,
};

View File

@ -2,7 +2,7 @@ import * as z from './types/base';
import { ZodDef } from '.';
import { ZodError, ZodErrorCode, ZodSuberror, ZodSuberrorOptionalMessage } from './ZodError';
import { util } from './helpers/util';
import { ZodErrorMap, defaultErrorMap } from './errorMap';
import { ZodErrorMap, defaultErrorMap } from './defaultErrorMap';
export type ParseParams = {
seen?: { schema: any; objects: any[] }[];
@ -10,7 +10,7 @@ export type ParseParams = {
errorMap?: ZodErrorMap;
};
export const getParsedType = (data: any): ParsedType => {
export const getParsedType = (data: any): ZodParsedType => {
if (typeof data === 'string') return 'string';
if (typeof data === 'number') {
if (Number.isNaN(data)) return 'nan';
@ -40,10 +40,11 @@ export const getParsedType = (data: any): ParsedType => {
return 'unknown';
};
export const ParsedType = util.arrayToEnum([
export const ZodParsedType = util.arrayToEnum([
'string',
'nan',
'number',
'integer',
'boolean',
'date',
'bigint',
@ -59,9 +60,10 @@ export const ParsedType = util.arrayToEnum([
]);
// export const ParsedType = arrayToEnum(ParsedTypeArray);
export type ParsedType = keyof typeof ParsedType;
export type ZodParsedType = keyof typeof ZodParsedType;
type StripErrorKeys<T extends object> = T extends any ? util.OmitKeys<T, 'path'> : never;
export type MakeErrorData = StripErrorKeys<ZodSuberrorOptionalMessage>;
export const ZodParser = (schemaDef: z.ZodTypeDef) => (
obj: any,
@ -84,21 +86,19 @@ export const ZodParser = (schemaDef: z.ZodTypeDef) => (
// },
};
const makeError = (
errorData: StripErrorKeys<ZodSuberrorOptionalMessage>,
// details: object = {},
): ZodSuberror => {
const makeError = (errorData: MakeErrorData): ZodSuberror => {
const errorArg = { ...errorData, path: params.path };
const ctxArg = { metadata: {}, data: obj };
const ctxArg = { data: obj };
const defaultError =
defaultErrorMap === params.errorMap
? `Invalid value`
? { message: `Invalid value.` }
: defaultErrorMap(errorArg, { ...ctxArg, defaultError: `Invalid value.` });
return {
...errorData,
path: params.path,
message: errorData.message || params.errorMap(errorArg, { ...ctxArg, defaultError }),
message:
errorData.message || params.errorMap(errorArg, { ...ctxArg, defaultError: defaultError.message }).message,
};
};
@ -141,34 +141,34 @@ export const ZodParser = (schemaDef: z.ZodTypeDef) => (
switch (def.t) {
case z.ZodTypes.string:
// error.addError()
if (parsedType !== ParsedType.string) {
if (parsedType !== ZodParsedType.string) {
//throw ZodError.fromString(`Non-string type: ${typeof obj}`);
error.addError(
makeError({ code: ZodErrorCode.invalid_type, expected: ParsedType.string, received: parsedType }),
makeError({ code: ZodErrorCode.invalid_type, expected: ZodParsedType.string, received: parsedType }),
);
throw error;
}
// return obj as any;
break;
case z.ZodTypes.number:
if (parsedType !== ParsedType.number) {
if (parsedType !== ZodParsedType.number) {
error.addError(
makeError({ code: ZodErrorCode.invalid_type, expected: ParsedType.number, received: parsedType }),
makeError({ code: ZodErrorCode.invalid_type, expected: ZodParsedType.number, received: parsedType }),
);
throw error;
}
if (Number.isNaN(obj)) {
error.addError(
makeError({ code: ZodErrorCode.invalid_type, expected: ParsedType.number, received: ParsedType.nan }),
makeError({ code: ZodErrorCode.invalid_type, expected: ZodParsedType.number, received: ZodParsedType.nan }),
);
throw error;
}
// return obj as any;
break;
case z.ZodTypes.bigint:
if (parsedType !== ParsedType.bigint) {
if (parsedType !== ZodParsedType.bigint) {
error.addError(
makeError({ code: ZodErrorCode.invalid_type, expected: ParsedType.number, received: parsedType }),
makeError({ code: ZodErrorCode.invalid_type, expected: ZodParsedType.number, received: parsedType }),
);
throw error;
// throw ZodError.fromString(`Non-bigint type: ${typeof obj}.`);
@ -176,19 +176,19 @@ export const ZodParser = (schemaDef: z.ZodTypeDef) => (
// return obj;
break;
case z.ZodTypes.boolean:
if (parsedType !== ParsedType.boolean) {
if (parsedType !== ZodParsedType.boolean) {
// throw ZodError.fromString(`Non-boolean type: ${typeof obj}`);
error.addError(
makeError({ code: ZodErrorCode.invalid_type, expected: ParsedType.boolean, received: parsedType }),
makeError({ code: ZodErrorCode.invalid_type, expected: ZodParsedType.boolean, received: parsedType }),
);
throw error;
}
// return obj as any;
break;
case z.ZodTypes.undefined:
if (parsedType !== ParsedType.undefined) {
if (parsedType !== ZodParsedType.undefined) {
error.addError(
makeError({ code: ZodErrorCode.invalid_type, expected: ParsedType.undefined, received: parsedType }),
makeError({ code: ZodErrorCode.invalid_type, expected: ZodParsedType.undefined, received: parsedType }),
);
throw error;
// throw ZodError.fromString(`Non-undefined type:Found: ${typeof obj}`);
@ -196,9 +196,11 @@ export const ZodParser = (schemaDef: z.ZodTypeDef) => (
// return undefined;
break;
case z.ZodTypes.null:
if (parsedType !== ParsedType.null) {
if (parsedType !== ZodParsedType.null) {
// throw ZodError.fromString(`Non-null type: ${typeof obj}`);
error.addError(makeError({ code: ZodErrorCode.invalid_type, expected: ParsedType.null, received: parsedType }));
error.addError(
makeError({ code: ZodErrorCode.invalid_type, expected: ZodParsedType.null, received: parsedType }),
);
throw error;
}
break;
@ -207,16 +209,18 @@ export const ZodParser = (schemaDef: z.ZodTypeDef) => (
case z.ZodTypes.unknown:
break;
case z.ZodTypes.void:
if (parsedType !== ParsedType.undefined && parsedType !== ParsedType.null) {
error.addError(makeError({ code: ZodErrorCode.invalid_type, expected: ParsedType.void, received: parsedType }));
if (parsedType !== ZodParsedType.undefined && parsedType !== ZodParsedType.null) {
error.addError(
makeError({ code: ZodErrorCode.invalid_type, expected: ZodParsedType.void, received: parsedType }),
);
throw error;
}
break;
case z.ZodTypes.array:
if (parsedType !== ParsedType.array) {
if (parsedType !== ZodParsedType.array) {
// throw ZodError.fromString(`Non-array type: ${typeof obj}`);
error.addError(
makeError({ code: ZodErrorCode.invalid_type, expected: ParsedType.array, received: parsedType }),
makeError({ code: ZodErrorCode.invalid_type, expected: ZodParsedType.array, received: parsedType }),
);
throw error;
}
@ -244,10 +248,10 @@ export const ZodParser = (schemaDef: z.ZodTypeDef) => (
break;
// return parsedArray as any;
case z.ZodTypes.object:
if (parsedType !== ParsedType.object) {
if (parsedType !== ZodParsedType.object) {
// throw ZodError.fromString(`Non-object type: ${typeof obj}`);
error.addError(
makeError({ code: ZodErrorCode.invalid_type, expected: ParsedType.object, received: parsedType }),
makeError({ code: ZodErrorCode.invalid_type, expected: ZodParsedType.object, received: parsedType }),
);
throw error;
}
@ -314,7 +318,7 @@ export const ZodParser = (schemaDef: z.ZodTypeDef) => (
error.addError(
makeError({
code: ZodErrorCode.invalid_union,
suberrors: unionErrors,
unionErrors: unionErrors,
}),
);
}
@ -350,17 +354,25 @@ export const ZodParser = (schemaDef: z.ZodTypeDef) => (
break;
case z.ZodTypes.tuple:
if (parsedType !== ParsedType.array) {
if (parsedType !== ZodParsedType.array) {
// tupleError.addError('','Non-array type detected; invalid tuple.')
error.addError(
makeError({ code: ZodErrorCode.invalid_type, expected: ParsedType.array, received: parsedType }),
makeError({ code: ZodErrorCode.invalid_type, expected: ZodParsedType.array, received: parsedType }),
);
throw error;
// throw ZodError.fromString('Non-array type detected; invalid tuple.');
}
if (def.items.length !== obj.length) {
if (obj.length > def.items.length) {
error.addError(
makeError({ code: ZodErrorCode.invalid_array_length, expected: def.items.length, received: obj.length }),
makeError({ code: ZodErrorCode.too_big, maximum: def.items.length, inclusive: true, type: 'array' }),
);
// tupleError.addError('',`Incorrect number of elements in tuple: expected ${def.items.length}, got ${obj.length}`)
// throw ZodError.fromString(
// `Incorrect number of elements in tuple: expected ${def.items.length}, got ${obj.length}`,
// );
} else if (obj.length < def.items.length) {
error.addError(
makeError({ code: ZodErrorCode.too_small, minimum: def.items.length, inclusive: true, type: 'array' }),
);
// tupleError.addError('',`Incorrect number of elements in tuple: expected ${def.items.length}, got ${obj.length}`)
// throw ZodError.fromString(
@ -417,11 +429,11 @@ export const ZodParser = (schemaDef: z.ZodTypeDef) => (
// return obj;
break;
case z.ZodTypes.function:
if (parsedType !== ParsedType.function) {
if (parsedType !== ZodParsedType.function) {
error.addError(
makeError({
code: ZodErrorCode.invalid_type,
expected: ParsedType.function,
expected: ZodParsedType.function,
received: parsedType,
}),
);
@ -437,7 +449,7 @@ export const ZodParser = (schemaDef: z.ZodTypeDef) => (
argsError.addError(
makeError({
code: ZodErrorCode.invalid_arguments,
suberrors: [err],
argumentsError: err,
}),
);
throw argsError;
@ -455,7 +467,7 @@ export const ZodParser = (schemaDef: z.ZodTypeDef) => (
returnsError.addError(
makeError({
code: ZodErrorCode.invalid_return_type,
suberrors: [err],
returnTypeError: err,
}),
);
throw returnsError;
@ -469,11 +481,11 @@ export const ZodParser = (schemaDef: z.ZodTypeDef) => (
return validatedFunc;
// return obj;
case z.ZodTypes.record:
if (parsedType !== ParsedType.object) {
if (parsedType !== ZodParsedType.object) {
error.addError(
makeError({
code: ZodErrorCode.invalid_type,
expected: ParsedType.object,
expected: ZodParsedType.object,
received: parsedType,
}),
);
@ -501,7 +513,7 @@ export const ZodParser = (schemaDef: z.ZodTypeDef) => (
error.addError(
makeError({
code: ZodErrorCode.invalid_type,
expected: ParsedType.date,
expected: ZodParsedType.date,
received: parsedType,
}),
);
@ -523,11 +535,11 @@ export const ZodParser = (schemaDef: z.ZodTypeDef) => (
break;
case z.ZodTypes.promise:
if (parsedType !== ParsedType.promise) {
if (parsedType !== ZodParsedType.promise) {
error.addError(
makeError({
code: ZodErrorCode.invalid_type,
expected: ParsedType.promise,
expected: ZodParsedType.promise,
received: parsedType,
}),
);
@ -568,14 +580,9 @@ export const ZodParser = (schemaDef: z.ZodTypeDef) => (
const customChecks = def.checks || [];
for (const check of customChecks) {
if (!check.check(obj)) {
error.addError(
makeError({
code: ZodErrorCode.custom_error,
params: check.params,
message: check.message,
}),
);
// throw ZodError.fromString(check.message || `Failed custom check.`);
const noMethodCheck = { ...check };
delete noMethodCheck.check;
error.addError(makeError(noMethodCheck));
}
}

View File

@ -0,0 +1,49 @@
import * as z from '.';
const run = async () => {
const errorMap: z.ZodErrorMap = (error, ctx) => {
/*
If error.message is set, that means the user is trying to
override the error message. This is how method-specific
error overrides work, like this:
z.string().min(5, { message: "TOO SMALL 🤬" })
It is a best practice to return `error.message` if it is set.
*/
if (error.message) return { message: error.message };
/*
This is where you override the various error codes
*/
switch (error.code) {
case z.ZodErrorCode.invalid_type:
if (error.expected === 'string') {
return { message: `This ain't a string!` };
}
break;
case z.ZodErrorCode.custom_error:
// produce a custom message using error.params
// error.params won't be set unless you passed
// a `params` arguments into a custom validator
const params = error.params || {};
if (params.myField) {
return { message: `Bad input: ${params.myField}` };
}
break;
}
// fall back to default message!
return { message: ctx.defaultError };
};
try {
z.string().parse(12, { errorMap });
} catch (err) {
console.log(JSON.stringify(err.errors, null, 2));
}
};
run();

View File

@ -2,6 +2,7 @@ import * as z from './base';
import { ZodUndefined } from './undefined';
import { ZodNull } from './null';
import { ZodUnion } from './union';
import { ZodErrorCode } from '..';
// import { maskUtil } from '../helpers/maskUtil';
// import { zodmaskUtil } from '../helpers/zodmaskUtil';
// import { applyMask } from '../masker';
@ -29,14 +30,31 @@ export class ZodArray<T extends z.ZodTypeAny> extends z.ZodType<T['_type'][], Zo
nullable: () => ZodUnion<[this, ZodNull]> = () => ZodUnion.create([this, ZodNull.create()]);
min = (minLength: number, msg?: string) =>
this.refine(data => data.length >= minLength, msg || `Array must contain ${minLength} or more items.`);
min = (minLength: number, message?: string | { message?: string }) =>
this._refinement({
check: data => data.length >= minLength,
code: ZodErrorCode.too_small,
type: 'array',
inclusive: true,
minimum: minLength,
...(typeof message === 'string' ? { message } : message),
});
max = (maxLength: number, msg?: string) =>
this.refine(data => data.length <= maxLength, msg || `Array must contain ${maxLength} or fewer items.`);
// this.refine(data => data.length >= minLength, msg || `Array must contain ${minLength} or more items.`);
length = (len: number, msg?: string) =>
this.refine(data => data.length == len, msg || `Array must contain ${len} items.`);
max = (maxLength: number, message?: string | { message?: string }) =>
this._refinement({
check: data => data.length <= maxLength,
code: ZodErrorCode.too_big,
type: 'array',
inclusive: true,
maximum: maxLength,
...(typeof message === 'string' ? { message } : message),
});
// this.refine(data => data.length <= maxLength, msg || `Array must contain ${maxLength} or fewer items.`);
length = (len: number, message?: string) => this.min(len, { message }).max(len, { message });
// .refine(data => data.length === len, msg || `Array must contain ${len} items.`);
nonempty: () => ZodNonEmptyArray<T> = () => {
return new ZodNonEmptyArray({ ...this._def, nonempty: true });
@ -71,6 +89,29 @@ export class ZodNonEmptyArray<T extends z.ZodTypeAny> extends z.ZodType<[T['_typ
nullable: () => ZodUnion<[this, ZodNull]> = () => ZodUnion.create([this, ZodNull.create()]);
min = (minLength: number, message?: string | { message?: string }) =>
this._refinement({
check: data => data.length >= minLength,
code: ZodErrorCode.too_small,
minimum: minLength,
type: 'array',
inclusive: true,
...(typeof message === 'string' ? { message } : message),
});
// this.refine(data => data.length >= minLength, msg || `Array must contain ${minLength} or more items.`);
max = (maxLength: number, message?: string | { message?: string }) =>
this._refinement({
check: data => data.length >= maxLength,
code: ZodErrorCode.too_big,
maximum: maxLength,
type: 'array',
inclusive: true,
...(typeof message === 'string' ? { message } : message),
});
length = (len: number, message?: string) => this.min(len, { message }).max(len, { message });
// static create = <T extends z.ZodTypeAny>(schema: T): ZodArray<T> => {
// return new ZodArray({
// t: z.ZodTypes.array,

View File

@ -1,4 +1,6 @@
import { ZodParser, ParseParams } from '../parser';
import { ZodParser, ParseParams, MakeErrorData } from '../parser';
import { util } from '../helpers/util';
import { ZodErrorCode } from '..';
export enum ZodTypes {
string = 'string',
@ -32,15 +34,19 @@ export type ZodRawShape = { [k: string]: ZodTypeAny };
// const asdf = { asdf: ZodString.create() };
// type tset1 = typeof asdf extends ZodRawShape ? true :false
type InternalCheck<T> = {
check: (arg: T) => any;
} & MakeErrorData;
type Check<T> = {
check: (arg: T) => any;
message?: string;
params?: { [k: string]: any };
// code: string
// code?: ZodErrorCode;
};
export interface ZodTypeDef {
t: ZodTypes;
checks?: Check<any>[];
checks?: InternalCheck<any>[];
}
export type TypeOf<T extends { _type: any }> = T['_type'];
@ -62,6 +68,18 @@ export abstract class ZodType<Type, Def extends ZodTypeDef = ZodTypeDef> {
parse: (x: Type | unknown, params?: ParseParams) => Type;
parseAsync: (x: Type | unknown, params?: ParseParams) => Promise<Type> = value => {
return new Promise((res, rej) => {
try {
const parsed = this.parse(value);
return res(parsed);
} catch (err) {
// console.log(err);
return rej(err);
}
});
};
is(u: Type): u is Type {
try {
this.parse(u as any);
@ -80,17 +98,21 @@ export abstract class ZodType<Type, Def extends ZodTypeDef = ZodTypeDef> {
}
}
refine = <Val extends (arg: Type) => any>(check: Val, message: string = 'Invalid value.') => {
// const newChecks = [...this._def.checks || [], { check, message }];
// console.log((this as any).constructor);
return new (this as any).constructor({
...this._def,
checks: [...(this._def.checks || []), { check, message }],
}) as this;
// return this;
refine = <Val extends (arg: Type) => any>(
check: Val,
message: string | util.Omit<Check<Type>, 'check'> = 'Invalid value.',
) => {
if (typeof message === 'string') {
return this.refinement({ check, message });
}
return this.refinement({ check, ...message });
};
refinement = (refinement: Check<Type>) => {
return this._refinement({ code: ZodErrorCode.custom_error, ...refinement });
};
protected _refinement: (refinement: InternalCheck<Type>) => this = refinement => {
return new (this as any).constructor({
...this._def,
checks: [...(this._def.checks || []), refinement],

View File

@ -2,6 +2,8 @@ import * as z from './base';
import { ZodUndefined } from './undefined';
import { ZodNull } from './null';
import { ZodUnion } from './union';
import { ZodErrorCode } from '..';
import { errorUtil } from '../helpers/errorUtil';
export interface ZodNumberDef extends z.ZodTypeDef {
t: z.ZodTypes.number;
@ -19,17 +21,79 @@ export class ZodNumber extends z.ZodType<number, ZodNumberDef> {
});
};
min = (minimum: number, msg?: string) => this.refine(data => data >= minimum, msg || `Value must be >= ${minimum}`);
// min = (minimum: number, msg?: string) => this.refine(data => data >= minimum, msg || `Value must be >= ${minimum}`);
min = (minimum: number, message?: errorUtil.ErrMessage) =>
this._refinement({
check: data => data >= minimum,
code: ZodErrorCode.too_small,
minimum,
type: 'number',
inclusive: true,
...errorUtil.errToObj(message),
});
max = (maximum: number, msg?: string) => this.refine(data => data <= maximum, msg || `Value must be <= ${maximum}`);
// max = (maximum: number, msg?: string) => this.refine(data => data <= maximum, msg || `Value must be <= ${maximum}`);
max = (maximum: number, message?: errorUtil.ErrMessage) =>
this._refinement({
check: data => data <= maximum,
code: ZodErrorCode.too_big,
maximum,
type: 'number',
inclusive: true,
...errorUtil.errToObj(message),
});
int = (msg?: string) => this.refine(data => Number.isInteger(data), msg || 'Value must be an integer.');
// int = (msg?: string) => this.refine(data => Number.isInteger(data), msg || 'Value must be an integer.');
int = (message?: errorUtil.ErrMessage) =>
this._refinement({
check: data => Number.isInteger(data),
code: ZodErrorCode.invalid_type,
expected: 'integer',
received: 'number',
...errorUtil.errToObj(message),
});
positive = (msg?: string) => this.refine(data => data > 0, msg || `Value must be positive`);
// positive = (msg?: string) => this.refine(data => data > 0, msg || `Value must be positive`);
positive = (message?: errorUtil.ErrMessage) =>
this._refinement({
check: data => data > 0,
code: ZodErrorCode.too_small,
minimum: 0,
type: 'number',
inclusive: false,
...errorUtil.errToObj(message),
});
negative = (msg?: string) => this.refine(data => data < 0, msg || `Value must be negative`);
// negative = (msg?: string) => this.refine(data => data < 0, msg || `Value must be negative`);
negative = (message?: errorUtil.ErrMessage) =>
this._refinement({
check: data => data < 0,
code: ZodErrorCode.too_big,
maximum: 0,
type: 'number',
inclusive: false,
...errorUtil.errToObj(message),
});
nonpositive = (msg?: string) => this.refine(data => data <= 0, msg || `Value must be non-positive`);
// nonpositive = (msg?: string) => this.refine(data => data <= 0, msg || `Value must be non-positive`);
nonpositive = (message?: errorUtil.ErrMessage) =>
this._refinement({
check: data => data <= 0,
code: ZodErrorCode.too_big,
maximum: 0,
type: 'number',
inclusive: true,
...errorUtil.errToObj(message),
});
nonnegative = (msg?: string) => this.refine(data => data >= 0, msg || `Value must be non-negative`);
// nonnegative = (msg?: string) => this.refine(data => data >= 0, msg || `Value must be non-negative`);
nonnegative = (message?: errorUtil.ErrMessage) =>
this._refinement({
check: data => data >= 0,
code: ZodErrorCode.too_small,
minimum: 0,
type: 'number',
inclusive: true,
...errorUtil.errToObj(message),
});
}

View File

@ -2,6 +2,8 @@ import * as z from './base';
import { ZodUndefined } from './undefined';
import { ZodNull } from './null';
import { ZodUnion } from './union';
import { ZodErrorCode } from '..';
import { errorUtil } from '../helpers/errorUtil';
// import { ParseParams } from '../parser';
export interface ZodStringDef extends z.ZodTypeDef {
@ -12,10 +14,9 @@ export interface ZodStringDef extends z.ZodTypeDef {
};
}
// eslint-disable-next-line
const emailRegex = /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i;
// eslint-disable-next-line
const urlRegex = /^((https?|ftp):)?\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i;
const uuidRegex = /([a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}){1}/g;
export class ZodString extends z.ZodType<string, ZodStringDef> {
optional: () => ZodUnion<[this, ZodUndefined]> = () => ZodUnion.create([this, ZodUndefined.create()]);
@ -24,20 +25,59 @@ export class ZodString extends z.ZodType<string, ZodStringDef> {
toJSON = () => this._def;
min = (minLength: number, msg?: string) =>
this.refine(data => data.length >= minLength, msg || `Value must be ${minLength} or more characters long`);
min = (minLength: number, message?: errorUtil.ErrMessage) =>
this._refinement({
check: data => data.length >= minLength,
code: ZodErrorCode.too_small,
minimum: minLength,
type: 'string',
inclusive: true,
...errorUtil.errToObj(message),
});
// this.refine(data => data.length >= minLength, msg || `Value must be ${minLength} or more characters long`);
max = (maxLength: number, msg?: string) =>
this.refine(data => data.length <= maxLength, msg || `Value must be ${maxLength} or fewer characters long`);
max = (maxLength: number, message?: errorUtil.ErrMessage) =>
this._refinement({
check: data => data.length <= maxLength,
code: ZodErrorCode.too_big,
maximum: maxLength,
type: 'string',
inclusive: true,
...errorUtil.errToObj(message),
});
// max = (maxLength: number, msg?: string) =>
// this.refine(data => data.length <= maxLength, msg || `Value must be ${maxLength} or fewer characters long`);
length = (len: number, message?: errorUtil.ErrMessage) => this.min(len, message).max(len, message);
// length = (len: number, msg?: string) =>
// this.refine(data => data.length == len, msg || `Value must be ${len} characters long.`);
length = (len: number, msg?: string) =>
this.refine(data => data.length == len, msg || `Value must be ${len} characters long.`);
email = (message?: errorUtil.ErrMessage) =>
this._refinement({
check: data => emailRegex.test(data),
code: ZodErrorCode.invalid_string,
validation: 'email',
...errorUtil.errToObj(message),
});
//this.refine(data => emailRegex.test(data), errorUtil.errToObj(message));
email = (msg?: string) => this.refine(data => emailRegex.test(data), msg || 'Invalid email address.');
url = (message?: errorUtil.ErrMessage) =>
this._refinement({
check: data => urlRegex.test(data),
code: ZodErrorCode.invalid_string,
validation: 'url',
...errorUtil.errToObj(message),
});
// url = (message?: errorUtil.ErrMessage) => this.refine(data => urlRegex.test(data), errorUtil.errToObj(message));
url = (msg?: string) => this.refine(data => urlRegex.test(data), msg || 'Invalid URL.');
uuid = (message?: errorUtil.ErrMessage) =>
this._refinement({
check: data => uuidRegex.test(data),
code: ZodErrorCode.invalid_string,
validation: 'uuid',
...errorUtil.errToObj(message),
});
nonempty = (msg?: string) => this.refine(data => data.length > 0, msg || 'Value cannot be empty string');
nonempty = (message?: errorUtil.ErrMessage) => this.min(1, errorUtil.errToObj(message));
// validate = <Val extends (arg:this['_type'])=>boolean>(check:Val)=>{
// const currChecks = this._def.validation.custom || [];
// return new ZodString({

View File

@ -3695,10 +3695,10 @@ typedarray-to-buffer@^3.1.5:
dependencies:
is-typedarray "^1.0.0"
typescript@3.9:
version "3.9.6"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.6.tgz#8f3e0198a34c3ae17091b35571d3afd31999365a"
integrity sha512-Pspx3oKAPJtjNwE92YS05HQoY7z2SFyOpHo9MqJor3BXAGNaPUs83CuVp9VISFkSjyRfiTpmKuAYGJB7S7hOxw==
typescript@3.2:
version "3.2.4"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.2.4.tgz#c585cb952912263d915b462726ce244ba510ef3d"
integrity sha512-0RNDbSdEokBeEAkgNbxJ+BLwSManFy9TeXz8uW+48j/xhEXv1ePME60olyzw2XzUqUBNAYFeJadIqAgNqIACwg==
undefsafe@^2.0.2:
version "2.0.3"