| name | zod-v4-patterns |
| description | Ensures Zod v4 patterns are used correctly throughout the codebase. Apply when creating or modifying validation schemas, form schemas, or any Zod validators. Enforces v4 syntax and prevents deprecated v3 patterns. |
| allowed-tools | Read, Edit, Write, Grep, Glob |
Zod v4 Patterns for kove-webapp
This skill ensures consistent usage of Zod v4 patterns and prevents deprecated v3 syntax.
When to Apply
Use these patterns when:
- Creating new validation schemas
- Defining form validation with React Hook Form
- Writing API request/response validators
- Updating existing Zod schemas from v3
- Adding custom error messages to validators
Critical Pattern Changes from v3 to v4
1. Error Customization (Most Important)
✅ v4: Use error parameter
z.string({ error: "Custom error message" })
z.number({ error: "Must be a number" })
z.boolean({ error: "Must be true or false" })
❌ v3 patterns (AVOID):
z.string({ message: "..." })
z.string({ invalid_type_error: "..." })
z.string({ required_error: "..." })
2. String Format Validators
✅ v4: Top-level functions
z.email()
z.email({ error: "Invalid email address" })
z.uuid()
z.uuid({ error: "Must be a valid UUID" })
z.url()
z.url({ error: "Must be a valid URL" })
❌ v3: Chained methods (AVOID)
z.string().email()
z.string().uuid()
z.string().url()
3. Object Strictness
✅ v4: Use constructors
z.strictObject({
name: z.string(),
age: z.number()
})
z.looseObject({
name: z.string(),
age: z.number()
})
z.object({
name: z.string(),
age: z.number()
})
❌ v3: Chained methods (AVOID)
z.object({ ... }).strict()
z.object({ ... }).passthrough()
4. Schema Composition
✅ v4: Use .extend()
const baseSchema = z.object({
name: z.string(),
email: z.email()
});
const extendedSchema = baseSchema.extend({
age: z.number(),
phone: z.string()
});
❌ v3: .merge() (AVOID)
const extendedSchema = baseSchema.merge(additionalSchema);
5. Default Values
⚠️ Important: .default() applies AFTER validation
The default value must match the output type, not the input type:
z.string().transform(s => s.length).default(5)
z.string().transform(s => s.length).default("hello")
Use .prefault() for pre-validation defaults:
z.string().prefault("default value")
6. Error Handling
✅ v4: Use z.prettifyError() or z.treeifyError()
const result = schema.safeParse(data);
if (!result.success) {
console.error(z.prettifyError(result.error));
const errorTree = z.treeifyError(result.error);
}
❌ v3: Avoid old methods
result.error.format()
result.error.flatten()
result.error.formErrors
Common Validation Patterns
Form Schema Example
import { z } from 'zod';
const formSchema = z.object({
name: z.string({ error: "Name is required" }),
email: z.email({ error: "Invalid email address" }),
password: z.string({ error: "Password is required" })
.min(8, { error: "Password must be at least 8 characters" }),
phone: z.string().optional(),
age: z.number({ error: "Age must be a number" })
.min(18, { error: "Must be at least 18 years old" })
.max(120, { error: "Invalid age" }),
terms: z.boolean({ error: "Must be true or false" })
.refine(val => val === true, {
:
}),
: z.([, , ], {
:
}),
: z.(z.({ : }))
.(, { : }),
: z.({
: z.({ : }),
: z.({ : }),
: z.({ : })
}),
: z.({ : })
});
= z.< formSchema>;
API Request Schema
const createLeaseSchema = z.strictObject({
propertyId: z.uuid({ error: "Invalid property ID" }),
tenantId: z.uuid({ error: "Invalid tenant ID" }),
startDate: z.string({ error: "Start date is required" })
.transform(str => new Date(str)),
endDate: z.string({ error: "End date is required" })
.transform(str => new Date(str)),
monthlyRent: z.number({ error: "Monthly rent must be a number" })
.positive({ error: "Monthly rent must be positive" }),
deposit: z.number({ error: "Deposit must be a number" })
.nonnegative({ error: "Deposit cannot be negative" })
}).refine(data => data.endDate > data.startDate, {
message: "End date must be after start date",
path: ["endDate"]
});
Server Action Validation
'use server';
import { z } from 'zod';
const inputSchema = z.object({
organizationId: z.uuid({ error: "Invalid organization ID" }),
name: z.string({ error: "Name is required" })
.min(1, { error: "Name cannot be empty" }),
email: z.email({ error: "Invalid email address" })
});
export async function createTenant(input: unknown) {
const result = inputSchema.safeParse(input);
if (!result.success) {
return {
error: z.prettifyError(result.error)
};
}
const validatedData = result.data;
}
React Hook Form Integration
'use client';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const formSchema = z.object({
email: z.email({ error: "Invalid email address" }),
password: z.string({ error: "Password is required" })
.min(8, { error: "Password must be at least 8 characters" })
});
type FormValues = z.infer<typeof formSchema>;
export function LoginForm() {
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
email: '',
password: ''
}
});
const onSubmit = async (data: FormValues) => {
};
return (
<form =>
{/* form fields */}
);
}
Migration Checklist
When updating schemas from v3 to v4:
Common Mistakes to Avoid
❌ Using v3 error syntax
z.string({ message: "Required" })
z.string({ invalid_type_error: "Must be string" })
z.string({ error: "Required" })
❌ Using chained format validators
z.string().email()
z.string().url()
z.email()
z.url()
❌ Using .merge() for composition
const extended = baseSchema.merge(additionalSchema);
const extended = baseSchema.extend({ ...additionalFields });
❌ Wrong default type
z.string().transform(s => parseInt(s)).default("0")
z.string().transform(s => parseInt(s)).default(0)
❌ Using deprecated error methods
if (!result.success) {
const errors = result.error.flatten();
}
if (!result.success) {
console.error(z.prettifyError(result.error));
}
Advanced Patterns
Custom Refinements
const passwordSchema = z.string({ error: "Password is required" })
.min(8, { error: "Password must be at least 8 characters" })
.refine(val => /[A-Z]/.test(val), {
message: "Password must contain at least one uppercase letter"
})
.refine(val => /[0-9]/.test(val), {
message: "Password must contain at least one number"
});
Conditional Validation
const schema = z.object({
type: z.enum(['individual', 'company']),
name: z.string({ error: "Name is required" }),
companyName: z.string().optional()
}).refine(data => {
if (data.type === 'company') {
return !!data.companyName;
}
return true;
}, {
message: "Company name is required for company type",
path: ["companyName"]
});
Discriminated Unions
const eventSchema = z.discriminatedUnion('type', [
z.object({
type: z.literal('click'),
x: z.number(),
y: z.number()
}),
z.object({
type: z.literal('keypress'),
key: z.string()
})
]);
Transform with Validation
const dateSchema = z.string({ error: "Date is required" })
.refine(val => !isNaN(Date.parse(val)), {
message: "Invalid date format"
})
.transform(val => new Date(val));
What to Check
When reviewing Zod schemas:
- ✅ Are error messages using
{ error: "..." } syntax?
- ✅ Are email/uuid/url validators using top-level functions?
- ✅ Are object strictness patterns using constructors?
- ✅ Is schema composition using
.extend()?
- ✅ Do
.default() values match output types?
- ✅ Is error handling using
z.prettifyError() or z.treeifyError()?
- ✅ Are there any deprecated v3 patterns?
- ✅ Are refinements used for complex validation logic?
- ✅ Are TypeScript types inferred with
z.infer<typeof schema>?
Resources
- Zod v4 Documentation: Check official docs for latest patterns
- Location: All schema definitions throughout the codebase
- Integration: React Hook Form uses
zodResolver for form validation