Every TypeScript project eventually needs runtime validation, because the type system disappears the moment data crosses a network boundary. Zod, Yup, and Valibot all solve that problem, but they make very different tradeoffs on bundle size, developer experience, and how tightly they integrate with your types. Here’s how to actually choose.
The one-line summary
- Zod: the default choice for most projects — great DX, huge ecosystem, first-class TypeScript inference.
- Yup: mature and battle-tested, especially strong in the React Formik/form-validation world, but weaker type inference than Zod.
- Valibot: built specifically to be small and tree-shakeable, aimed at projects where bundle size is a hard constraint.
Bundle size and DX compared
| Library | Min+gzip (typical usage) | Type inference | API style |
|---|---|---|---|
| Zod | ~12-14kb (whole lib imported) | Excellent, native z.infer |
Chainable, object-oriented |
| Yup | ~15-18kb | Good, but sometimes needs manual type assertions | Chainable, schema-builder |
| Valibot | ~1-3kb (only what you import) | Excellent, similar to Zod | Functional, composable pipes |
Valibot’s tree-shaking advantage is real: because every validator is a separate function you import individually, unused validators never ship to the browser. Zod is improving this in newer versions but still ships larger by default.
Code comparison: the same schema, three ways
Zod:
import { z } from 'zod';
const UserSchema = z.object({
name: z.string().min(2),
email: z.string().email(),
age: z.number().int().positive().optional(),
});
type User = z.infer<typeof UserSchema>;
const result = UserSchema.safeParse(input);
Valibot:
import * as v from 'valibot';
const UserSchema = v.object({
name: v.pipe(v.string(), v.minLength(2)),
email: v.pipe(v.string(), v.email()),
age: v.optional(v.pipe(v.number(), v.integer(), v.minValue(1))),
});
type User = v.InferOutput<typeof UserSchema>;
const result = v.safeParse(UserSchema, input);
Yup:
import * as yup from 'yup';
const UserSchema = yup.object({
name: yup.string().min(2).required(),
email: yup.string().email().required(),
age: yup.number().integer().positive().optional(),
});
const result = await UserSchema.validate(input).catch((e) => e);
When to pick which
- Backend API validation, general TypeScript apps: Zod. It’s the ecosystem default — most tRPC, form libraries, and AI SDKs already have Zod integrations built in.
- Existing Formik-based forms, legacy codebase: stick with Yup rather than migrating for marginal gains.
- Edge functions, mobile web, or anywhere bundle size is scored: Valibot, since its per-function imports keep shipped code minimal.
What doesn’t show up in benchmarks
- Zod’s ecosystem size means more Stack Overflow answers, more library integrations, and fewer surprises when something goes wrong.
- Valibot is younger, so expect occasional gaps in third-party integrations compared to Zod.
- Runtime validation cost (CPU, not bundle size) is similar across all three for typical payload sizes — don’t over-optimize for this unless you’re validating at very high throughput.
If you’re migrating an existing JavaScript codebase to add this kind of type safety in the first place, see how to learn JavaScript in 2026 without getting overwhelmed for the foundations before adding a validation layer on top.
Quick FAQ
Can I use more than one of these in the same project?
Technically yes, but it adds cognitive overhead and duplicate bundle weight. Pick one as your standard and stick to it.
Is Valibot production-ready?
Yes, it’s stable and used in production by teams that prioritize bundle size, though its ecosystem of third-party integrations is smaller than Zod’s.
Does switching validation libraries require rewriting all my schemas?
Usually yes — the APIs are similar in spirit but not compatible syntax, so budget real migration time rather than expecting a drop-in swap.
Leave a Reply