Skip to content

Commit 4b8255d

Browse files
author
Colin McDonnell
committed
Implemented path overrides in .refine
1 parent b2b51e7 commit 4b8255d

7 files changed

Lines changed: 150 additions & 80 deletions

File tree

README.md

Lines changed: 62 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,12 @@ If you find this package useful, leave a star to help more folks find it ⭐️
3434
- [Strings](#strings)
3535
- [Numbers](#numbers)
3636
- [Objects](#objects)
37-
- [.nonstrict](#unknown-keys)
37+
- [.shape](#shape-property)
3838
- [.merge](#merging)
39-
- [.extend](#extending)
39+
- [.extend](#extending-objects)
4040
- [.pick/.omit](#masking)
4141
- [.partial/.deepPartial](#partials)
42+
- [.nonstrict](#unknown-keys)
4243
- [Records](#records)
4344
- [Arrays](#arrays)
4445
- [.nonempty](#non-empty-lists)
@@ -184,28 +185,68 @@ const process = (blob: any) => {
184185
};
185186
```
186187

187-
To learn more about error handling with Zod, jump to [Errors](#errors).
188-
189188
### Custom validation
190189

191-
`.refine(validator: (data:T)=>any, err?: string)`
190+
`.refine(validator: (data:T)=>any, params?: RefineParams)`
192191

193192
Zod was designed to mirror TypeScript as closely as possible. But there are many so-called "refinement types" you may wish to check for that can't be represented in TypeScript's type system. For instance: checking that a number is an Int or that a string is a valid email address.
194193

195194
For this instances, you can define custom a validation check on _any_ Zod schema with `.refine`:
196195

197196
```ts
198-
const myString = z.string().refine(val => val.length <= 255, "String can't be more than 255 characters");
197+
const myString = z.string().refine(val => val.length <= 255, {
198+
message: "String can't be more than 255 characters",
199+
});
199200
```
200201

201-
Here is the type signature for `.refine`:
202-
203202
As you can see, `.refine` takes two arguments.
204203

205204
1. The first is the validation function. This function takes one input (of type `T` — the inferred type of the schema) and returns `any`. Any truthy value will pass validation. (Prior to zod@1.6.2 the validation function had to return a boolean.)
206-
2. The second argument is a custom error message. Read more about error handling in Zod [here](#errors)
205+
2. The second argument is a params object. You can use this to customize certain error-handling behavior:
206+
207+
```ts
208+
type RefineParams = {
209+
// override error message
210+
message?: string;
211+
212+
// override error path
213+
path?: (string | number)[];
214+
215+
// params object you can use to customize message
216+
// in error map
217+
params?: object;
218+
};
219+
```
220+
221+
These params let you define powerful custom behavior. Zod is commonly used for form validation. If you want to verify that "password" and "confirmPassword" match, you can do so like this:
222+
223+
```ts
224+
z.object({
225+
password: z.string(),
226+
confirm: z.string(),
227+
})
228+
.refine(data => data.confirm === data.password, {
229+
message: "Passwords don't match",
230+
path: ['confirm'],
231+
})
232+
.parse({ password: 'asdf', confirmPassword: 'qwer' });
233+
```
207234

208-
Check out [validator.js](https://github.com/validatorjs/validator.js) for a bunch of useful string validation functions.
235+
Because you provided a `path` parameter, the resulting error will be:
236+
237+
```ts
238+
ZodError {
239+
errors: [{
240+
"code": "custom_error",
241+
"path": [ "confirm" ],
242+
"message": "Invalid input."
243+
}]
244+
}
245+
```
246+
247+
Note that the `path` is set to `["confirm"]`, so you can easily display this error underneath the "Confirm password" textbox.
248+
249+
j
209250

210251
## Type inference
211252

@@ -236,6 +277,8 @@ z.string().url();
236277
z.string().uuid();
237278
```
238279

280+
> Check out [validator.js](https://github.com/validatorjs/validator.js) for a bunch of other useful string validation functions.
281+
239282
### Custom error messages
240283

241284
Like `.refine`, The final (optional) argument is an object that lets you provide a custom error in the `message` field.
@@ -255,11 +298,9 @@ z.string().uuid({ message: 'Invalid UUID' });
255298

256299
There are a handful of number-specific validations.
257300

258-
The final (optional) argument is a params object that lets you provide a custom error in the `message` field.
259-
260301
```ts
261302
z.number().min(5);
262-
z.number().max(5, { message: 'this👏is👏too👏big' });
303+
z.number().max(5);
263304

264305
z.number().int(); // value must be an integer
265306

@@ -269,6 +310,12 @@ z.number().negative(); // < 0
269310
z.number().nonpositive(); // <= 0
270311
```
271312

313+
You can optionally pass in a params object as the second argument to provide a custom error message.
314+
315+
```ts
316+
z.number().max(5, { message: 'this👏is👏too👏big' });
317+
```
318+
272319
## Objects
273320

274321
```ts
@@ -352,9 +399,9 @@ type Merged = z.infer<typeof merged>;
352399

353400
To "overwrite" existing keys, use `.extend` (documented below).
354401

355-
#### Augmentation
402+
#### Extending objects
356403

357-
You can augment an object schema with the `.extend` method.
404+
You can add additional fields an object schema with the `.extend` method.
358405

359406
> Before zod@1.8 this method was called `.augment`. The `augment` method is still available for backwards compatibility but it is deprecated and will be removed in a future release.
360407

coverage.svg

Lines changed: 1 addition & 1 deletion
Loading

src/ZodError.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ interface TooBigError extends ZodSuberrorBase {
119119
type: 'array' | 'string' | 'number';
120120
}
121121

122-
interface CustomError extends ZodSuberrorBase {
122+
export interface CustomError extends ZodSuberrorBase {
123123
code: typeof ZodErrorCode.custom_error;
124124
params?: { [k: string]: any };
125125
}
@@ -182,4 +182,6 @@ export class ZodError extends Error {
182182
addErrors = (subs: ZodSuberror[] = []) => {
183183
this.errors = [...this.errors, ...subs];
184184
};
185+
186+
// toFormError =
185187
}

src/__tests__/refine.test.ts

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,32 @@ test('refinement', () => {
2020
});
2121

2222
test('refinement 2', () => {
23-
try {
24-
const validationSchema = z
25-
.object({
26-
email: z.string().email(),
27-
password: z.string(),
28-
confirmPassword: z.string(),
29-
})
30-
.refine(data => data.password === data.confirmPassword, 'Both password and confirmation must match');
23+
const validationSchema = z
24+
.object({
25+
email: z.string().email(),
26+
password: z.string(),
27+
confirmPassword: z.string(),
28+
})
29+
.refine(data => data.password === data.confirmPassword, 'Both password and confirmation must match');
3130

31+
expect(() =>
3232
validationSchema.parse({
3333
email: 'aaaa@gmail.com',
3434
password: 'aaaaaaaa',
3535
confirmPassword: 'bbbbbbbb',
36+
}),
37+
).toThrow();
38+
});
39+
40+
test('custom path', () => {
41+
z.object({
42+
password: z.string(),
43+
confirm: z.string(),
44+
})
45+
.refine(data => data.confirm === data.password, { path: ['confirm'] })
46+
.parseAsync({ password: 'asdf', confirm: 'qewr' })
47+
.catch(err => {
48+
console.log(JSON.stringify(err, null, 2));
49+
expect(err.errors[0].path).toEqual(['confirm']);
3650
});
37-
} catch (err) {
38-
console.log(JSON.stringify(err.errors, null, 2));
39-
}
4051
});

src/parser.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export const ZodParsedType = util.arrayToEnum([
6363
export type ZodParsedType = keyof typeof ZodParsedType;
6464

6565
type StripErrorKeys<T extends object> = T extends any ? util.OmitKeys<T, 'path'> : never;
66-
export type MakeErrorData = StripErrorKeys<ZodSuberrorOptionalMessage>;
66+
export type MakeErrorData = StripErrorKeys<ZodSuberrorOptionalMessage> & { path?: (string | number)[] };
6767

6868
export const ZodParser = (schemaDef: z.ZodTypeDef) => (
6969
obj: any,
@@ -96,7 +96,7 @@ export const ZodParser = (schemaDef: z.ZodTypeDef) => (
9696
: defaultErrorMap(errorArg, { ...ctxArg, defaultError: `Invalid value.` });
9797
return {
9898
...errorData,
99-
path: params.path,
99+
path: [...params.path, ...(errorData.path || [])],
100100
message:
101101
errorData.message || params.errorMap(errorArg, { ...ctxArg, defaultError: defaultError.message }).message,
102102
};

src/playground.ts

Lines changed: 55 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,57 @@
11
import * as z from '.';
22

3-
const run = async () => {
4-
const errorMap: z.ZodErrorMap = (error, ctx) => {
5-
/*
6-
7-
If error.message is set, that means the user is trying to
8-
override the error message. This is how method-specific
9-
error overrides work, like this:
10-
11-
z.string().min(5, { message: "TOO SMALL 🤬" })
12-
13-
It is a best practice to return `error.message` if it is set.
14-
15-
*/
16-
if (error.message) return { message: error.message };
17-
18-
/*
19-
This is where you override the various error codes
20-
*/
21-
switch (error.code) {
22-
case z.ZodErrorCode.invalid_type:
23-
if (error.expected === 'string') {
24-
return { message: `This ain't a string!` };
25-
}
26-
break;
27-
case z.ZodErrorCode.custom_error:
28-
// produce a custom message using error.params
29-
// error.params won't be set unless you passed
30-
// a `params` arguments into a custom validator
31-
const params = error.params || {};
32-
if (params.myField) {
33-
return { message: `Bad input: ${params.myField}` };
34-
}
35-
break;
36-
}
37-
38-
// fall back to default message!
39-
return { message: ctx.defaultError };
40-
};
41-
42-
try {
43-
z.string().parse(12, { errorMap });
44-
} catch (err) {
45-
console.log(JSON.stringify(err.errors, null, 2));
46-
}
47-
};
48-
49-
run();
3+
z.object({
4+
password: z.string(),
5+
confirm: z.string(),
6+
})
7+
.refine(data => data.confirm === data.password, { path: ['confirm'] })
8+
.parseAsync({ password: 'asdf', confirm: 'qewr' })
9+
.catch(err => console.log(JSON.stringify(err, null, 2)));
10+
11+
// const run = async () => {
12+
// const errorMap: z.ZodErrorMap = (error, ctx) => {
13+
// /*
14+
15+
// If error.message is set, that means the user is trying to
16+
// override the error message. This is how method-specific
17+
// error overrides work, like this:
18+
19+
// z.string().min(5, { message: "TOO SMALL 🤬" })
20+
21+
// It is a best practice to return `error.message` if it is set.
22+
23+
// */
24+
// if (error.message) return { message: error.message };
25+
26+
// /*
27+
// This is where you override the various error codes
28+
// */
29+
// switch (error.code) {
30+
// case z.ZodErrorCode.invalid_type:
31+
// if (error.expected === 'string') {
32+
// return { message: `This ain't a string!` };
33+
// }
34+
// break;
35+
// case z.ZodErrorCode.custom_error:
36+
// // produce a custom message using error.params
37+
// // error.params won't be set unless you passed
38+
// // a `params` arguments into a custom validator
39+
// const params = error.params || {};
40+
// if (params.myField) {
41+
// return { message: `Bad input: ${params.myField}` };
42+
// }
43+
// break;
44+
// }
45+
46+
// // fall back to default message!
47+
// return { message: ctx.defaultError };
48+
// };
49+
50+
// try {
51+
// z.string().parse(12, { errorMap });
52+
// } catch (err) {
53+
// console.log(JSON.stringify(err.errors, null, 2));
54+
// }
55+
// };
56+
57+
// run();

src/types/base.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { ZodParser, ParseParams, MakeErrorData } from '../parser';
22
import { util } from '../helpers/util';
33
import { ZodErrorCode } from '..';
4+
import { CustomError } from '../ZodError';
45

56
export enum ZodTypes {
67
string = 'string',
@@ -40,10 +41,11 @@ type InternalCheck<T> = {
4041

4142
type Check<T> = {
4243
check: (arg: T) => any;
43-
message?: string;
44-
params?: { [k: string]: any };
44+
// message?: string;
45+
path?: (string | number)[];
46+
// params?: { [k: string]: any };
4547
// code?: ZodErrorCode;
46-
};
48+
} & util.Omit<CustomError, 'code' | 'path'>;
4749
export interface ZodTypeDef {
4850
t: ZodTypes;
4951
checks?: InternalCheck<any>[];

0 commit comments

Comments
 (0)