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 →