Zod is the standard for TypeScript validation. But it ships 14KB to the browser. Valibot does the same thing in less than 1KB thanks to tree-shaking.
The Size Difference
| Library | Full bundle | After tree-shaking (basic schema) |
|---|---|---|
| Zod | 14.3KB | 14.3KB (no tree-shaking) |
| Valibot | 8.7KB | 0.5-1KB (fully tree-shakeable) |
Zod uses a class-based API (methods on objects). Classes cannot be tree-shaken. Valibot uses functions — bundlers remove what you do not use.
Basic Validation
import * as v from "valibot";
// Define schema
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.minValue(0), v.maxValue(150)),
role: v.picklist(["admin", "user", "editor"]),
bio: v.optional(v.pipe(v.string(), v.maxLength(500))),
});
// Infer TypeScript type
type User = v.InferOutput<typeof UserSchema>;
// { name: string; email: string; age: number; role: "admin" | "user" | "editor"; bio?: string }
// Validate
const result = v.safeParse(UserSchema, input);
if (result.success) {
console.log(result.output); // Typed as User
} else {
console.log(result.issues); // Detailed error messages
}
Zod vs Valibot Comparison
// Zod
const schema = z.object({
name: z.string().min(2).max(50),
email: z.string().email(),
age: z.number().min(0),
});
// Valibot — very similar!
const schema = 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.minValue(0)),
});
The main difference: Valibot uses pipe() for chaining validations instead of method chaining.
Advanced Schemas
Unions and Variants
const EventSchema = v.variant("type", [
v.object({ type: v.literal("click"), x: v.number(), y: v.number() }),
v.object({ type: v.literal("keypress"), key: v.string() }),
v.object({ type: v.literal("scroll"), deltaY: v.number() }),
]);
Transformations
const DateSchema = v.pipe(
v.string(),
v.isoTimestamp(),
v.transform((input) => new Date(input)),
);
// Input: string → Output: Date
Recursive Types
type Comment = { text: string; replies: Comment[] };
const CommentSchema: v.GenericSchema<Comment> = v.object({
text: v.string(),
replies: v.lazy(() => v.array(CommentSchema)),
});
Framework Integration
React Hook Form
import { useForm } from "react-hook-form";
import { valibotResolver } from "@hookform/resolvers/valibot";
const form = useForm({
resolver: valibotResolver(UserSchema),
});
tRPC
const router = t.router({
createUser: t.procedure
.input(v.parser(UserSchema))
.mutation(({ input }) => { /* input is typed */ }),
});
When to Use Valibot vs Zod
Choose Valibot when:
- Bundle size matters (frontend, edge, mobile)
- You only use a few validations (tree-shaking saves most)
- Performance is critical (Valibot is faster in benchmarks)
Choose Zod when:
- Ecosystem integration (more libraries support Zod directly)
- Team familiarity
- Method chaining preference (
.string().email()vspipe(string(), email()))
Building type-safe applications? I create developer tools and data solutions. Email spinov001@gmail.com or explore my Apify tools.
Top comments (0)