Zod Schema Patterns for SvelteKit Forms
Essential Zod Patterns for Real-World Forms
Zod is the validator of choice for SvelteKit form libraries because it's TypeScript-native, composable, and works identically on client and server. Here are the patterns you'll use most often when building production forms.
Basic String Validation
Email with Domain Restrictions
const schema = z.object({
email: z.string()
.email('Please enter a valid email address')
.refine(
email => !email.endsWith('@example.com'),
'Please use your work email, not example.com'
)
});
The .email() check handles format validation. The .refine() adds a custom rule — useful for blocking disposable email domains or requiring corporate addresses.
Password with Strength Requirements
const schema = z.object({
password: z.string()
.min(8, 'Password must be at least 8 characters')
.regex(/[A-Z]/, 'Must include at least one uppercase letter')
.regex(/[a-z]/, 'Must include at least one lowercase letter')
.regex(/[0-9]/, 'Must include at least one number')
});
Each .regex() check runs independently and produces its own error message. Users see all unmet requirements at once, not one at a time.
Number and Date Fields
Coerced Number Input
HTML form inputs always send strings. Use z.coerce.number() to automatically parse the string to a number:
const schema = z.object({
age: z.coerce.number()
.min(18, 'Must be at least 18 years old')
.max(120, 'Please enter a valid age'),
quantity: z.coerce.number()
.int('Must be a whole number')
.positive('Must be greater than 0')
});
Without z.coerce, a form input sending "25" would fail Zod's number validation because it's a string. Coercion handles the parsing transparently.
Date Range Validation
const schema = z.object({
startDate: z.string().date('Please enter a valid date'),
endDate: z.string().date('Please enter a valid date')
}).refine(
data => new Date(data.endDate) > new Date(data.startDate),
{ message: 'End date must be after start date', path: ['endDate'] }
);
Select, Radio, and Enum Fields
Enum Validation for Select/Radio
const schema = z.object({
role: z.enum(['admin', 'editor', 'viewer'], {
errorMap: () => ({ message: 'Please select a role' })
}),
priority: z.enum(['low', 'medium', 'high', 'critical'])
});
Using z.enum() instead of z.string() for select fields means only the defined values are accepted. This prevents form tampering where someone edits the HTML to add a fake option value.
Checkbox Groups and Arrays
Multi-Select with Min/Max
const schema = z.object({
interests: z.array(z.enum(['design', 'engineering', 'marketing', 'sales']))
.min(1, 'Select at least one interest')
.max(3, 'Select at most 3 interests')
});
Checkbox groups send arrays. Wrapping the enum in z.array() with .min() and .max() gives you selection count validation.
File Upload Validation
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
const schema = z.object({
avatar: z.instanceof(File)
.refine(file => file.size <= MAX_FILE_SIZE, 'File must be under 5MB')
.refine(
file => ['image/jpeg', 'image/png', 'image/webp'].includes(file.type),
'Only JPEG, PNG, and WebP images are accepted'
)
});
File validation with Zod uses z.instanceof(File) and .refine() for size and type checks. Note that this only works on the client — server-side file handling in SvelteKit requires parsing the multipart form data separately.
Cross-Field Validation
Password Confirmation
const schema = z.object({
password: z.string().min(8, 'Password must be at least 8 characters'),
confirmPassword: z.string()
}).refine(
data => data.password === data.confirmPassword,
{ message: 'Passwords do not match', path: ['confirmPassword'] }
);
The .refine() at the object level receives the full form data, so it can compare fields. The path option controls which field shows the error.
Complex Business Rules with superRefine
const schema = z.object({
contactMethod: z.enum(['email', 'phone']),
email: z.string().optional(),
phone: z.string().optional()
}).superRefine((data, ctx) => {
if (data.contactMethod === 'email' && !data.email) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Email is required when contact method is email',
path: ['email']
});
}
if (data.contactMethod === 'phone' && !data.phone) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Phone is required when contact method is phone',
path: ['phone']
});
}
});
Use .superRefine() when you need to add multiple conditional issues. Unlike .refine(), it can add issues to multiple paths in a single pass.
Optional Fields with Transforms
const schema = z.object({
phone: z.string()
.transform(s => s.replace(/\D/g, ''))
.pipe(z.string().length(10, 'Phone number must be 10 digits'))
.optional(),
website: z.string()
.url('Please enter a valid URL')
.optional()
.or(z.literal(''))
});
The .transform().pipe() chain strips non-digit characters then validates the result. The .optional().or(z.literal('')) pattern allows empty strings from form inputs to pass validation — without it, an empty optional URL field would fail the .url() check.
Generate Schemas Automatically
Writing these patterns by hand is time-consuming and error-prone. SvelteForms generates Zod schemas automatically from a visual form builder. Configure your fields, set validation rules, and export a production-ready schema.ts file.
Try the SvelteKit form builder or browse generated code examples to see the output for common form patterns like contact forms, signup flows, and surveys.
Build forms faster
Try the form builder →