17.2 Runtime Type Validation — Mastering Zod and Comparing Valibot/ArkType
Zod — TypeScript-first Schema Validation
npm install zod
Basic Schemas
import { z } from 'zod'
// Basic type schemas
const StringSchema = z.string()
const NumberSchema = z.number()
const BooleanSchema = z.boolean()
// Parsing — throws ZodError on failure
const name = StringSchema.parse('Alice') // 'Alice'
const age = NumberSchema.parse('25') // ZodError!
// Safe parsing — returns success: false on failure
const result = NumberSchema.safeParse('not a number')
if (!result.success) {
console.log(result.error.issues)
// [{ code: 'invalid_type', expected: 'number', received: 'string' }]
}
Object Schemas
import { z } from 'zod'
const UserSchema = z.object({
id: z.string().uuid(),
email: z.string().email('Please enter a valid email address.'),
name: z.string().min(2, 'Name must be at least 2 characters.').max(50),
age: z.number().int().min(0).max(150).optional(),
role: z.enum(['admin', 'user', 'guest']).default('user'),
tags: z.array(z.string()).max(10).optional(),
address: z.object({
city: z.string(),
country: z.string().length(2),
}).optional(),
createdAt: z.date().default(() => new Date()),
})
// Type inference
type User = z.infer<typeof UserSchema>
// {
// id: string;
// email: string;
// name: string;
// age?: number;
// role: 'admin' | 'user' | 'guest';
// ...
// }
// Parsing
const user = UserSchema.parse({
id: '550e8400-e29b-41d4-a716-446655440000',
email: 'alice@example.com',
name: 'Alice',
})
Advanced Zod Patterns
import { z } from 'zod'
// Union type
const Shape = z.discriminatedUnion('kind', [
z.object({ kind: z.literal('circle'), radius: z.number() }),
z.object({ kind: z.literal('rectangle'), width: z.number(), height: z.number() }),
])
// Transform
const StringToNumber = z.string().transform((val) => parseInt(val, 10))
type StringToNumber = z.infer<typeof StringToNumber> // number
// Refine
const PasswordSchema = z.string()
.min(8)
.refine(
(val) => /[A-Z]/.test(val),
{ message: 'Must contain at least one uppercase letter.' }
)
.refine(
(val) => /[0-9]/.test(val),
{ message: 'Must contain at least one number.' }
)
// Cross-field validation (superRefine)
const ConfirmPasswordSchema = z.object({
password: z.string().min(8),
confirmPassword: z.string(),
}).superRefine((data, ctx) => {
if (data.password !== data.confirmPassword) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Passwords do not match.',
path: ['confirmPassword'],
})
}
})
// Partial schemas
const PartialUserSchema = UserSchema.partial() // All optional
const RequiredUserSchema = UserSchema.required() // All required
const PickedSchema = UserSchema.pick({ name: true, email: true })
const OmittedSchema = UserSchema.omit({ password: true })
// Extending
const AdminUserSchema = UserSchema.extend({
permissions: z.array(z.string()),
lastLoginAt: z.date().nullable(),
})
z.infer and Type Sharing
import { z } from 'zod'
// Define API schemas in one place
export const CreatePostSchema = z.object({
title: z.string().min(1).max(200),
content: z.string().min(10),
tags: z.array(z.string()).optional(),
published: z.boolean().default(false),
})
// Auto-generate TypeScript type
export type CreatePostDto = z.infer<typeof CreatePostSchema>
// Frontend — integrate with react-hook-form
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
function PostForm() {
const form = useForm<CreatePostDto>({
resolver: zodResolver(CreatePostSchema),
})
// ...
}
// Backend — Express middleware
function validateBody<T extends z.ZodType>(schema: T) {
return (req: Request, res: Response, next: NextFunction) => {
const result = schema.safeParse(req.body)
if (!result.success) {
return res.status(400).json({
errors: result.error.flatten().fieldErrors,
})
}
req.body = result.data
next()
}
}
app.post('/posts', validateBody(CreatePostSchema), createPostHandler)
Valibot — Lightweight Alternative
npm install valibot
import * as v from 'valibot'
// Similar to Zod but smaller bundle size
const UserSchema = v.object({
id: v.string([v.uuid()]),
email: v.string([v.email()]),
name: v.string([v.minLength(2), v.maxLength(50)]),
age: v.optional(v.number([v.integer(), v.minValue(0)])),
})
type User = v.Output<typeof UserSchema>
const result = v.safeParse(UserSchema, { id: '...', email: '...', name: 'Alice' })
if (result.success) {
console.log(result.output)
}
Pro Tips
Zod vs Valibot vs ArkType Selection Guide
Zod:
- Widest ecosystem (tRPC, react-hook-form, Prisma integration)
- Best documentation and community
- Bundle size 13KB+ gzipped
Valibot:
- Tree-shaking optimized (only bundle what you use)
- Edge runtime (Cloudflare Workers, etc.)
- Zod-like API
ArkType:
- Uses TypeScript syntax directly
- Best performance (compile-time optimization)
- New paradigm (learning curve)