zod-v4-patterns

Zod v4 Patterns for kove-webapp

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "zod-v4-patterns" with this command: npx skills add jchaselubitz/drill-app/jchaselubitz-drill-app-zod-v4-patterns

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: "..." })

  1. 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()

  1. 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()

  1. 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);

  1. 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")

  1. 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() with z.email()

  • Replace z.string().uuid() with z.uuid()

  • Replace z.string().url() with z.url()

  • Replace .strict() with z.strictObject()

  • Replace .passthrough() with z.looseObject()

  • Replace .merge() with .extend()

  • Replace .format() with z.prettifyError()

  • Replace .flatten() with z.prettifyError() or z.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() 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

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

expo-router

No summary provided by upstream source.

Repository SourceNeeds Review
General

expo-audio

No summary provided by upstream source.

Repository SourceNeeds Review
General

expo-glass-effect

No summary provided by upstream source.

Repository SourceNeeds Review