Every application needs input validation. Forms, API endpoints, environment variables, and configuration files all need data shape verification. Three libraries dominate this space, each with different design philosophies.
Philosophy
Zod: TypeScript-first schema declaration and validation. "Define a schema, infer the TypeScript type." Object-oriented API.
Yup: Battle-tested validation library inspired by Joi. Established in the React form ecosystem (Formik). Method-chaining API.
Valibot: Modular, tree-shakeable validation. Functional API where each validator is a separate import. Smallest bundle size.
Bundle Size
| Library | Full Bundle | Typical Usage | Tree-Shaken |
|---|---|---|---|
| Zod | 57 KB (minified) | 57 KB | Not tree-shakeable |
| Yup | 40 KB (minified) | 40 KB | Partially |
| Valibot | 60 KB (full) | 5-10 KB | Fully tree-shakeable |
Zod's non-tree-shakeable design means you ship the full library even if you use 10% of it. Valibot's modular design means you only ship the validators you import.
API Comparison
Defining a User Schema
Zod:
import { z } from 'zod'
const userSchema = z.object({
name: z.string().min(2).max(50),
email: z.string().email(),
age: z.number().int().min(18).max(120),
role: z.enum(['admin', 'user', 'editor']),
})
type User = z.infer<typeof userSchema>
Yup:
import * as yup from 'yup'
const userSchema = yup.object({
name: yup.string().required().min(2).max(50),
email: yup.string().required().email(),
age: yup.number().required().integer().min(18).max(120),
role: yup.string().required().oneOf(['admin', 'user', 'editor']),
})
type User = yup.InferType<typeof userSchema>
Valibot:
import * as v from 'valibot'
const userSchema = v.object({
name: v.pipe(v.string(), v.minLength(2), v.maxLength(50)),
email: v.pipe(v.string(), v.email()),
age: v.pipe(v.number(), v.integer(), v.minValue(18), v.maxValue(120)),
role: v.picklist(['admin', 'user', 'editor']),
})
type User = v.InferOutput<typeof userSchema>
All three produce equivalent TypeScript types and validation behavior. The API style differs:
- Zod: method chaining on schema objects
- Yup: method chaining with
.required()as default opt-in - Valibot: functional composition with
pipe()
Feature Comparison
| Feature | Zod | Yup | Valibot |
|---|---|---|---|
| TypeScript inference | Excellent | Good | Excellent |
| Tree-shaking | No | Partial | Full |
| Custom error messages | Yes | Yes | Yes |
| Async validation | Yes | Yes | Yes |
| Transforms | Yes (transform, preprocess) | Yes (transform) | Yes (transform) |
| Discriminated unions | Yes | Limited | Yes |
| Recursive schemas | Yes | Limited | Yes |
| Coercion | z.coerce.* | Built-in cast | v.transform() |
| Form library integration | React Hook Form, Conform | Formik, React Hook Form | React Hook Form, Conform |
| Default values | .default() | .default() | v.optional(schema, default) |
| Error formatting | .format(), .flatten() | .errors, paths | v.flatten() |
Performance
Validating the same object 10,000 times:
| Library | Time | Ops/Second |
|---|---|---|
| Valibot | 12ms | 833,000 |
| Zod | 28ms | 357,000 |
| Yup | 45ms | 222,000 |
Valibot is the fastest. Zod is 2x slower. Yup is 4x slower. For typical use (validating a form or API request), the difference is imperceptible. For high-throughput servers, it matters.
Form Integration
With React Hook Form
All three work via resolvers:
// Zod
import { zodResolver } from '@hookform/resolvers/zod'
useForm({ resolver: zodResolver(userSchema) })
// Yup
import { yupResolver } from '@hookform/resolvers/yup'
useForm({ resolver: yupResolver(userSchema) })
// Valibot
import { valibotResolver } from '@hookform/resolvers/valibot'
useForm({ resolver: valibotResolver(userSchema) })
With Server Actions (Next.js)
// Zod
const result = userSchema.safeParse(formData)
// Valibot
const result = v.safeParse(userSchema, formData)
// Yup
try {
const result = await userSchema.validate(formData)
} catch (error) { /* handle */ }
Zod and Valibot return a result object (success/failure pattern). Yup throws on failure, requiring try/catch.
Ecosystem
| Metric | Zod | Yup | Valibot |
|---|---|---|---|
| npm weekly downloads | 25M+ | 10M+ | 1.5M+ |
| GitHub stars | 35K+ | 22K+ | 6K+ |
| Community tutorials | Many | Many | Growing |
| Form library support | Best (official resolvers) | Good (Formik native) | Good (resolvers) |
| tRPC integration | Native | Third-party | Third-party |
| Nuxt/SvelteKit support | Good | Good | Good |
When to Choose Each
Zod
- New TypeScript projects (most popular, best ecosystem)
- tRPC users (native integration)
- Maximum community support (most tutorials, answers)
- Complex schemas (discriminated unions, recursive types)
- Teams prioritizing DX over bundle size
Yup
- Existing Formik projects (native integration)
- Familiar to the team (switching has a cost)
- Legacy codebases being maintained, not rebuilt
- Cast/coercion needs (Yup's casting is built-in and mature)
Valibot
- Bundle size matters (client-heavy apps, mobile web)
- Performance-critical server validation
- Functional programming style preference
- Edge/serverless where cold start size matters
- New projects willing to adopt a newer tool
Our Choice
We use Zod as our default. The ecosystem integration (React Hook Form, tRPC, Next.js server actions), community support, and TypeScript inference quality make it the pragmatic choice. For projects where bundle size is critical, we evaluate Valibot.
Contact us to discuss your application architecture.