zod-v4-patterns
4
总安装量
2
周安装量
#53177
全站排名
安装命令
npx skills add https://github.com/jchaselubitz/drill-app --skill zod-v4-patterns
Agent 安装分布
opencode
2
codex
2
claude-code
2
gemini-cli
2
replit
1
windsurf
1
Skill 文档
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
// Strict: No unknown keys allowed
z.strictObject({
name: z.string(),
age: z.number()
})
// Loose: Allow unknown keys to pass through
z.looseObject({
name: z.string(),
age: z.number()
})
// Default object (strips unknown keys)
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:
// â
Correct: Default matches output type
z.string().transform(s => s.length).default(5)
// â Wrong: Default doesn't match output (output is number)
z.string().transform(s => s.length).default("hello")
Use .prefault() for pre-validation defaults:
// Applies default BEFORE validation
z.string().prefault("default value")
6. Error Handling
â
v4: Use z.prettifyError() or z.treeifyError()
const result = schema.safeParse(data);
if (!result.success) {
// Pretty print errors for debugging
console.error(z.prettifyError(result.error));
// Or get tree structure
const errorTree = z.treeifyError(result.error);
}
â v3: Avoid old methods
result.error.format() // Deprecated
result.error.flatten() // Deprecated
result.error.formErrors // Deprecated
Common Validation Patterns
Form Schema Example
import { z } from 'zod';
const formSchema = z.object({
// Basic string with custom error
name: z.string({ error: "Name is required" }),
// Email validation
email: z.email({ error: "Invalid email address" }),
// String with minimum length
password: z.string({ error: "Password is required" })
.min(8, { error: "Password must be at least 8 characters" }),
// Optional field
phone: z.string().optional(),
// Number with range
age: z.number({ error: "Age must be a number" })
.min(18, { error: "Must be at least 18 years old" })
.max(120, { error: "Invalid age" }),
// Boolean with default
terms: z.boolean({ error: "Must be true or false" })
.refine(val => val === true, {
message: "You must accept the terms and conditions"
}),
// Enum
role: z.enum(['admin', 'user', 'guest'], {
error: "Invalid role selected"
}),
// Array of strings
tags: z.array(z.string({ error: "Each tag must be a string" }))
.min(1, { error: "At least one tag is required" }),
// Nested object
address: z.object({
street: z.string({ error: "Street is required" }),
city: z.string({ error: "City is required" }),
zip: z.string({ error: "ZIP code is required" })
}),
// UUID
userId: z.uuid({ error: "Invalid user ID format" })
});
type FormData = z.infer<typeof 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) {
// Validate input
const result = inputSchema.safeParse(input);
if (!result.success) {
return {
error: z.prettifyError(result.error)
};
}
const validatedData = result.data;
// Use validatedData (fully typed)
// ...
}
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) => {
// data is fully typed and validated
};
return (
<form onSubmit={form.handleSubmit(onSubmit)}>
{/* form fields */}
</form>
);
}
Migration Checklist
When updating schemas from v3 to v4:
- Replace
{ message: "..." }with{ error: "..." } - Replace
{ invalid_type_error: "..." }with{ error: "..." } - Replace
{ required_error: "..." }with{ error: "..." } - Replace
z.string().email()withz.email() - Replace
z.string().uuid()withz.uuid() - Replace
z.string().url()withz.url() - Replace
.strict()withz.strictObject() - Replace
.passthrough()withz.looseObject() - Replace
.merge()with.extend() - Replace
.format()withz.prettifyError() - Replace
.flatten()withz.prettifyError()orz.treeifyError() - Verify
.default()values match output types - Consider using
.prefault()for pre-validation defaults
Common Mistakes to Avoid
â Using v3 error syntax
// Wrong
z.string({ message: "Required" })
z.string({ invalid_type_error: "Must be string" })
// Correct
z.string({ error: "Required" })
â Using chained format validators
// Wrong
z.string().email()
z.string().url()
// Correct
z.email()
z.url()
â Using .merge() for composition
// Wrong
const extended = baseSchema.merge(additionalSchema);
// Correct
const extended = baseSchema.extend({ ...additionalFields });
â Wrong default type
// Wrong: default doesn't match output type (number)
z.string().transform(s => parseInt(s)).default("0")
// Correct: default matches output type
z.string().transform(s => parseInt(s)).default(0)
â Using deprecated error methods
// Wrong
if (!result.success) {
const errors = result.error.flatten();
}
// Correct
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()orz.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
zodResolverfor form validation