Skip to main content
Advertisement

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)
Advertisement