0
# Refinements and Custom Validation
1
2
Add custom validation logic to schemas with synchronous or asynchronous checks.
3
4
## Refine Method
5
6
Boolean validation checks with custom error messages.
7
8
```typescript { .api }
9
interface ZodType<Output, Input> {
10
refine(
11
check: (value: Output) => boolean | Promise<boolean>,
12
params?: string | {
13
message?: string;
14
path?: (string | number)[];
15
params?: object;
16
}
17
): this;
18
}
19
```
20
21
**Examples:**
22
```typescript
23
// Simple refine
24
z.number().refine((val) => val > 0, "Must be positive")
25
z.string().refine((val) => val.includes("@"), "Invalid email format")
26
27
// With custom path
28
z.string().refine(
29
(val) => val.length >= 8,
30
{
31
message: "Must be at least 8 characters",
32
path: ["credentials", "password"],
33
}
34
)
35
36
// Multiple refine calls
37
z.string()
38
.refine((val) => val.length >= 8, "At least 8 characters")
39
.refine((val) => /[A-Z]/.test(val), "Must contain uppercase")
40
.refine((val) => /[a-z]/.test(val), "Must contain lowercase")
41
.refine((val) => /[0-9]/.test(val), "Must contain number")
42
.refine((val) => /[!@#$%^&*]/.test(val), "Must contain special char")
43
44
// Refine on objects (cross-field validation)
45
z.object({
46
password: z.string(),
47
confirmPassword: z.string(),
48
}).refine(
49
(data) => data.password === data.confirmPassword,
50
{
51
message: "Passwords don't match",
52
path: ["confirmPassword"],
53
}
54
)
55
56
// Async refine (requires parseAsync)
57
z.string().refine(
58
async (username) => {
59
const exists = await checkUsernameExists(username);
60
return !exists;
61
},
62
{ message: "Username already taken" }
63
)
64
65
// Must use:
66
await schema.parseAsync(data);
67
```
68
69
## SuperRefine Method
70
71
Advanced refinement with context for adding multiple issues.
72
73
```typescript { .api }
74
interface ZodType<Output, Input> {
75
superRefine(
76
refineFn: (value: Output, ctx: RefinementCtx) => void | Promise<void>
77
): this;
78
}
79
80
interface RefinementCtx {
81
addIssue(issue: IssueData): void;
82
path: (string | number)[];
83
}
84
85
interface IssueData {
86
code: ZodIssueCode;
87
message?: string;
88
path?: (string | number)[];
89
fatal?: boolean; // Stops validation immediately
90
[key: string]: any;
91
}
92
```
93
94
**Examples:**
95
```typescript
96
// Multiple validation issues
97
z.string().superRefine((val, ctx) => {
98
if (val.length < 8) {
99
ctx.addIssue({
100
code: z.ZodIssueCode.too_small,
101
minimum: 8,
102
type: "string",
103
inclusive: true,
104
message: "At least 8 characters",
105
});
106
}
107
108
if (!/[A-Z]/.test(val)) {
109
ctx.addIssue({
110
code: z.ZodIssueCode.custom,
111
message: "Must contain uppercase",
112
});
113
}
114
115
if (!/[0-9]/.test(val)) {
116
ctx.addIssue({
117
code: z.ZodIssueCode.custom,
118
message: "Must contain number",
119
});
120
}
121
})
122
123
// Conditional validation
124
z.object({
125
type: z.enum(["personal", "business"]),
126
taxId: z.string().optional(),
127
}).superRefine((data, ctx) => {
128
if (data.type === "business" && !data.taxId) {
129
ctx.addIssue({
130
code: z.ZodIssueCode.custom,
131
message: "Tax ID required for business accounts",
132
path: ["taxId"],
133
});
134
}
135
})
136
137
// Cross-field validation
138
z.object({
139
startDate: z.date(),
140
endDate: z.date(),
141
}).superRefine((data, ctx) => {
142
if (data.endDate <= data.startDate) {
143
ctx.addIssue({
144
code: z.ZodIssueCode.custom,
145
message: "End date must be after start date",
146
path: ["endDate"],
147
});
148
}
149
})
150
151
// Fatal issues (stop validation immediately)
152
z.string().superRefine((val, ctx) => {
153
if (val.includes("forbidden")) {
154
ctx.addIssue({
155
code: z.ZodIssueCode.custom,
156
message: "Forbidden content detected",
157
fatal: true, // Stops validation
158
});
159
}
160
})
161
162
// Async super refine
163
z.string().superRefine(async (val, ctx) => {
164
const isValid = await validateWithAPI(val);
165
if (!isValid) {
166
ctx.addIssue({
167
code: z.ZodIssueCode.custom,
168
message: "Validation failed",
169
});
170
}
171
})
172
```
173
174
## Check Method
175
176
Reusable validation check functions.
177
178
```typescript { .api }
179
interface ZodType<Output, Input> {
180
check(...checks: Array<(value: Output) => boolean | Promise<boolean>>): this;
181
}
182
```
183
184
**Examples:**
185
```typescript
186
// Single check
187
z.number().check((val) => val % 2 === 0)
188
189
// Multiple checks
190
z.number()
191
.check((val) => val > 0)
192
.check((val) => val < 100)
193
.check((val) => val % 2 === 0)
194
195
// Reusable check functions
196
const isPositive = (val: number) => val > 0;
197
const isEven = (val: number) => val % 2 === 0;
198
199
z.number().check(isPositive, isEven)
200
```
201
202
## Overwrite Method
203
204
Transform values during validation (in-place modification).
205
206
```typescript { .api }
207
interface ZodType<Output, Input> {
208
overwrite(transformFn: (value: Output) => Output): ZodEffects<this>;
209
}
210
```
211
212
**Examples:**
213
```typescript
214
// Normalize email
215
z.string()
216
.email()
217
.overwrite((val) => val.toLowerCase().trim())
218
219
// Remove forbidden characters
220
z.string()
221
.overwrite((val) => val.replace(/[<>]/g, ""))
222
223
// Conditional overwrite
224
z.object({
225
name: z.string(),
226
autoGenerate: z.boolean(),
227
}).overwrite((data) => {
228
if (data.autoGenerate) {
229
return { ...data, name: `Generated_${Date.now()}` };
230
}
231
return data;
232
})
233
```
234
235
## Common Patterns
236
237
```typescript
238
// Password validation
239
const PasswordSchema = z
240
.string()
241
.refine((val) => val.length >= 8, "At least 8 characters")
242
.refine((val) => /[A-Z]/.test(val), "Must contain uppercase")
243
.refine((val) => /[a-z]/.test(val), "Must contain lowercase")
244
.refine((val) => /[0-9]/.test(val), "Must contain number");
245
246
// Confirm password pattern
247
const PasswordFormSchema = z
248
.object({
249
password: z.string().min(8),
250
confirmPassword: z.string(),
251
})
252
.refine((data) => data.password === data.confirmPassword, {
253
message: "Passwords don't match",
254
path: ["confirmPassword"],
255
});
256
257
// Async username availability
258
const UsernameSchema = z.string().refine(
259
async (username) => {
260
const available = await checkAvailability(username);
261
return available;
262
},
263
{ message: "Username already taken" }
264
);
265
266
// Must use: await schema.parseAsync(data);
267
268
// Date range validation
269
const DateRangeSchema = z
270
.object({
271
start: z.date(),
272
end: z.date(),
273
})
274
.refine((data) => data.end > data.start, {
275
message: "End date must be after start date",
276
path: ["end"],
277
});
278
279
// Conditional required field
280
const AccountSchema = z
281
.object({
282
type: z.enum(["personal", "business"]),
283
taxId: z.string().optional(),
284
})
285
.superRefine((data, ctx) => {
286
if (data.type === "business" && !data.taxId) {
287
ctx.addIssue({
288
code: z.ZodIssueCode.custom,
289
message: "Tax ID required for business",
290
path: ["taxId"],
291
});
292
}
293
});
294
295
// Complex validation with multiple issues
296
const ComplexSchema = z.object({ data: z.any() }).superRefine((val, ctx) => {
297
// Add multiple issues
298
// Access context path
299
// Conditional validation
300
// Fatal errors
301
});
302
303
// Combine with transformations
304
const ValidateAndTransform = z
305
.string()
306
.trim()
307
.refine((val) => val.length > 0)
308
.transform((val) => val.toUpperCase());
309
```
310
311
## Best Practices
312
313
```typescript
314
// Use refine for simple boolean checks
315
z.string().refine((val) => val.length > 0)
316
317
// Use superRefine for:
318
// - Multiple issues
319
// - Complex validation
320
// - Conditional validation
321
// - Access to context path
322
323
// Async refinements require parseAsync
324
const asyncSchema = z.string().refine(async (val) => {
325
return await checkAvailability(val);
326
});
327
await asyncSchema.parseAsync(data); // Required
328
329
// Error handling with refinements
330
const result = z
331
.number()
332
.refine((val) => val > 0, "Must be positive")
333
.safeParse(-1);
334
335
if (!result.success) {
336
console.log(result.error.issues[0].message); // "Must be positive"
337
}
338
```
339