153 lines
4.1 KiB
Markdown
153 lines
4.1 KiB
Markdown
---
|
|
title: Choose refine() vs superRefine() Correctly
|
|
impact: MEDIUM
|
|
impactDescription: refine() only reports one error; superRefine() enables multiple issues and custom error codes
|
|
tags: refine, superRefine, validation, custom
|
|
---
|
|
|
|
## Choose refine() vs superRefine() Correctly
|
|
|
|
`.refine()` is for simple single-condition validation returning boolean. `.superRefine()` gives you a context object to add multiple issues with custom error codes and paths. Choose based on your error reporting needs.
|
|
|
|
**Incorrect (using refine for multiple checks):**
|
|
|
|
```typescript
|
|
import { z } from 'zod'
|
|
|
|
// refine can only report one error at a time
|
|
const passwordSchema = z.string().refine(
|
|
(password) => {
|
|
// Checks all conditions but only reports first failure
|
|
if (password.length < 8) return false // Only this error shown
|
|
if (!/[A-Z]/.test(password)) return false
|
|
if (!/[0-9]/.test(password)) return false
|
|
return true
|
|
},
|
|
{ message: 'Password does not meet requirements' }
|
|
)
|
|
|
|
passwordSchema.parse('weak')
|
|
// Only shows: "Password does not meet requirements"
|
|
// User doesn't know WHICH requirements failed
|
|
```
|
|
|
|
**Correct (using superRefine for multiple issues):**
|
|
|
|
```typescript
|
|
import { z } from 'zod'
|
|
|
|
const passwordSchema = z.string().superRefine((password, ctx) => {
|
|
if (password.length < 8) {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
message: 'Password must be at least 8 characters',
|
|
})
|
|
}
|
|
|
|
if (!/[A-Z]/.test(password)) {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
message: 'Password must contain an uppercase letter',
|
|
})
|
|
}
|
|
|
|
if (!/[0-9]/.test(password)) {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
message: 'Password must contain a number',
|
|
})
|
|
}
|
|
|
|
if (!/[!@#$%^&*]/.test(password)) {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
message: 'Password must contain a special character',
|
|
})
|
|
}
|
|
})
|
|
|
|
passwordSchema.safeParse('weak')
|
|
// Shows ALL failures:
|
|
// - "Password must be at least 8 characters"
|
|
// - "Password must contain an uppercase letter"
|
|
// - "Password must contain a number"
|
|
// - "Password must contain a special character"
|
|
```
|
|
|
|
**When to use refine():**
|
|
|
|
```typescript
|
|
// Simple boolean condition with one error message
|
|
const adultSchema = z.number().refine(
|
|
(age) => age >= 18,
|
|
{ message: 'Must be 18 or older' }
|
|
)
|
|
|
|
// Cross-field validation with single outcome
|
|
const formSchema = z.object({
|
|
password: z.string(),
|
|
confirmPassword: z.string(),
|
|
}).refine(
|
|
(data) => data.password === data.confirmPassword,
|
|
{ message: 'Passwords must match', path: ['confirmPassword'] }
|
|
)
|
|
|
|
// Async validation
|
|
const emailSchema = z.string().email().refine(
|
|
async (email) => {
|
|
const exists = await checkEmailExists(email)
|
|
return !exists
|
|
},
|
|
{ message: 'Email already registered' }
|
|
)
|
|
```
|
|
|
|
**When to use superRefine():**
|
|
|
|
```typescript
|
|
// Multiple independent checks on same value
|
|
// Cross-field validation with multiple possible errors
|
|
// Need custom error codes for i18n or client handling
|
|
// Need to add issues at specific paths
|
|
|
|
const orderSchema = z.object({
|
|
items: z.array(z.object({
|
|
productId: z.string(),
|
|
quantity: z.number(),
|
|
})),
|
|
promoCode: z.string().optional(),
|
|
}).superRefine(async (order, ctx) => {
|
|
// Check each item's availability
|
|
for (let i = 0; i < order.items.length; i++) {
|
|
const item = order.items[i]
|
|
const available = await checkInventory(item.productId, item.quantity)
|
|
|
|
if (!available) {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
path: ['items', i, 'quantity'], // Specific path
|
|
message: `Only ${available} units available`,
|
|
})
|
|
}
|
|
}
|
|
|
|
// Validate promo code
|
|
if (order.promoCode) {
|
|
const valid = await validatePromoCode(order.promoCode)
|
|
if (!valid) {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
path: ['promoCode'],
|
|
message: 'Invalid or expired promo code',
|
|
})
|
|
}
|
|
}
|
|
})
|
|
```
|
|
|
|
**When NOT to use this pattern:**
|
|
- Simple single-condition checks (use refine for simplicity)
|
|
- Transform needed instead of validation (use transform)
|
|
|
|
Reference: [Zod API - refine/superRefine](https://zod.dev/api#refine)
|