142 lines
3.7 KiB
Markdown
142 lines
3.7 KiB
Markdown
---
|
|
title: Add Path to Refinement Errors
|
|
impact: MEDIUM
|
|
impactDescription: Errors without path show at object level; adding path highlights the specific field that failed
|
|
tags: refine, path, errors, forms
|
|
---
|
|
|
|
## Add Path to Refinement Errors
|
|
|
|
When using `.refine()` on object schemas for cross-field validation, add a `path` option to indicate which field the error relates to. Without it, the error appears at the object level, making form error display confusing.
|
|
|
|
**Incorrect (error at object level):**
|
|
|
|
```typescript
|
|
import { z } from 'zod'
|
|
|
|
const formSchema = z.object({
|
|
password: z.string().min(8),
|
|
confirmPassword: z.string(),
|
|
}).refine(
|
|
(data) => data.password === data.confirmPassword,
|
|
{ message: 'Passwords do not match' } // No path specified
|
|
)
|
|
|
|
const result = formSchema.safeParse({
|
|
password: 'secret123',
|
|
confirmPassword: 'different',
|
|
})
|
|
|
|
if (!result.success) {
|
|
const flattened = result.error.flatten()
|
|
// {
|
|
// formErrors: ['Passwords do not match'], // At form level!
|
|
// fieldErrors: {} // Empty - no field association
|
|
// }
|
|
}
|
|
|
|
// Form UI can't highlight which field has the error
|
|
```
|
|
|
|
**Correct (error with path):**
|
|
|
|
```typescript
|
|
import { z } from 'zod'
|
|
|
|
const formSchema = z.object({
|
|
password: z.string().min(8),
|
|
confirmPassword: z.string(),
|
|
}).refine(
|
|
(data) => data.password === data.confirmPassword,
|
|
{
|
|
message: 'Passwords do not match',
|
|
path: ['confirmPassword'], // Error appears on this field
|
|
}
|
|
)
|
|
|
|
const result = formSchema.safeParse({
|
|
password: 'secret123',
|
|
confirmPassword: 'different',
|
|
})
|
|
|
|
if (!result.success) {
|
|
const flattened = result.error.flatten()
|
|
// {
|
|
// formErrors: [],
|
|
// fieldErrors: {
|
|
// confirmPassword: ['Passwords do not match'] // Associated with field
|
|
// }
|
|
// }
|
|
}
|
|
|
|
// Form can now show error next to confirmPassword input
|
|
```
|
|
|
|
**Multiple cross-field validations:**
|
|
|
|
```typescript
|
|
const dateRangeSchema = z.object({
|
|
startDate: z.coerce.date(),
|
|
endDate: z.coerce.date(),
|
|
minDays: z.number().optional(),
|
|
maxDays: z.number().optional(),
|
|
}).refine(
|
|
(data) => data.endDate >= data.startDate,
|
|
{ message: 'End date must be after start date', path: ['endDate'] }
|
|
).refine(
|
|
(data) => {
|
|
if (!data.minDays) return true
|
|
const days = (data.endDate.getTime() - data.startDate.getTime()) / 86400000
|
|
return days >= data.minDays
|
|
},
|
|
{ message: 'Date range is too short', path: ['endDate'] }
|
|
).refine(
|
|
(data) => {
|
|
if (!data.maxDays) return true
|
|
const days = (data.endDate.getTime() - data.startDate.getTime()) / 86400000
|
|
return days <= data.maxDays
|
|
},
|
|
{ message: 'Date range is too long', path: ['endDate'] }
|
|
)
|
|
```
|
|
|
|
**With superRefine for multiple path errors:**
|
|
|
|
```typescript
|
|
const orderSchema = z.object({
|
|
billingAddress: z.object({
|
|
street: z.string(),
|
|
city: z.string(),
|
|
}),
|
|
shippingAddress: z.object({
|
|
street: z.string(),
|
|
city: z.string(),
|
|
}),
|
|
sameAsBilling: z.boolean(),
|
|
}).superRefine((data, ctx) => {
|
|
if (data.sameAsBilling) {
|
|
// If sameAsBilling but addresses differ, show errors on shipping
|
|
if (data.shippingAddress.street !== data.billingAddress.street) {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
message: 'Must match billing address',
|
|
path: ['shippingAddress', 'street'], // Nested path
|
|
})
|
|
}
|
|
if (data.shippingAddress.city !== data.billingAddress.city) {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
message: 'Must match billing address',
|
|
path: ['shippingAddress', 'city'],
|
|
})
|
|
}
|
|
}
|
|
})
|
|
```
|
|
|
|
**When NOT to use this pattern:**
|
|
- When the error genuinely applies to the whole object
|
|
- Simple single-field refinements (path is implicit)
|
|
|
|
Reference: [Zod API - refine](https://zod.dev/api#refine)
|