Zod vs Valibot
The two TypeScript-first schema validation libraries. Both: define a schema, parse data, get a typed result or typed errors. The philosophical split is object-method API vs function-pipe API, and everything else (bundle size, tree-shaking, ergonomics) follows from it.
The same schema, both dialects
Zod — methods on schema objects:
import { z } from "zod";
const User = z.object({
id: z.string().uuid(),
name: z.string().min(1).max(80),
email: z.string().email(),
role: z.enum(["admin", "user"]).default("user"),
tags: z.array(z.string()),
});
type User = z.infer<typeof User>; // static type, inferred
const result = User.safeParse(input); // { success, data | error }Valibot — small independent functions, piped:
import * as v from "valibot";
const User = v.object({
id: v.pipe(v.string(), v.uuid()),
name: v.pipe(v.string(), v.minLength(1), v.maxLength(80)),
email: v.pipe(v.string(), v.email()),
role: v.optional(v.picklist(["admin", "user"]), "user"),
tags: v.array(v.string()),
});
type User = v.InferOutput<typeof User>; // static type, inferred
const result = v.safeParse(User, input); // { success, output | issues }The core difference, visually
Zod bundles capability into classes; Valibot composes from standalone functions. That single choice drives tree-shaking:
zod import:
import { z } from "zod"
└── z is a namespace OBJECT ── bundler must keep the object graph
└── even if you use 5 methods, dead-code elimination is partial
valibot import:
import { object, string, pipe, uuid } from "valibot"
└── named ES exports ── bundler drops every function you don't import
└── unused validators never reach the bundleMeasured (bundlephobia, 2026-09, zod 4.6.5 vs valibot 1.5.0):
- zod: 385.5 KB min / 88.6 KB gzip (full import)
- valibot: 84.8 KB min / 14.7 KB gzip full — and < 0.7 KB when tree-shaken to a few validators (their pitch: up to 95% smaller than Zod in real bundles)
Side by side
| Axis | zod (OSS) | valibot (OSS) |
|---|---|---|
| API style | object methods, .min(), .default() | function pipes, pipe(x, y) |
| Version | 4.6.5 | 1.5.0 |
| Stars | 44.0k | 9.0k |
| Weekly npm downloads | ~341M | ~23M |
| Bundle (gzip, full) | 88.6 KB | 14.7 KB |
| Tree-shaking | partial | the design goal (< 0.7 KB minimal) |
| Ecosystem | the de-facto standard: tRPC, React Hook Form, OpenAPI tooling, LLM structured-output libs all assume it | adapters exist for most, but Zod is the default assumption |
| Errors | .safeParse → single ZodError with path array | safeParse → issues[] (multiple by design) |
| Custom messages | .min(1, "required") inline | v.minLength(1, "required") inline — same shape |
| Docs/maturity | v4 rewrite shipped (faster, smaller than v3), huge cookbook | v1 stable, excellent docs, growing |
Strengths and weaknesses
Zod
- ✅ Ecosystem gravity — everything TypeScript-schema-adjacent speaks Zod first; least integration friction
- ✅ Method chaining reads top-to-bottom for common cases; the 90% schema is terse
- ✅ v4 addressed the worst performance/bundle critiques directly
- ❌ Full-import weight matters in edge runtimes (serverless cold starts, browser)
- ❌ The class surface makes aggressive tree-shaking structurally hard
Valibot
- ✅ Tree-shaking is the identity: pay only for validators you import
- ✅ Pipe model separates type (shape) from actions (checks/transforms) — explicit and composable
- ✅ Multi-issue output by default (better form UX)
- ❌ Pipe syntax is noisier for simple schemas; method muscle-memory must unlearn
- ❌ Ecosystem plays adapter tax: libraries that take “a Zod schema” need a conversion or plugin
- ❌ Smaller community = fewer Stack Overflow answers for edge cases
How to choose
choosing a TS schema library:
+ server-side / Node API backends → zod (ecosystem, nothing watches bundle size)
+ edge runtimes / serverless / client bundles → valibot (bytes are the budget)
+ library that ACCEPTS user schemas → zod (the format users already have)
+ library that SHIPS its own schemas to the client → valibot (tree-shaking pays)The 2026 context: Zod’s weekly download numbers are AI-ecosystem scale (structured-output tooling standardized on it) — network effects keep compounding. Valibot is the technically cleaner core design winning where bytes matter. For garden/homelab projects (Hermes config UIs, viz pages), either works; the tiebreaker is whether the code runs in a browser bundle.
The Python sibling: pydantic
pydantic (OSS) (28.9k★, MIT) plays the zod role in Python — 812M monthly pip downloads (vs zod’s 341M weekly npm), FastAPI’s validation layer, the de-facto Python schema standard. The interesting inversion is the direction of truth:
zod / valibot (schema-first):
const User = z.object({...})
- type User = z.infer<typeof User> # type DERIVED from schema
pydantic (type-first):
class User(BaseModel):
id: str
name: str = Field(min_length=1, max_length=80)
+ User.model_validate(input) # schema DERIVED from annotations
+ User.model_json_schema() # and exported when neededPydantic validates from your type annotations (constraints ride along in Field); zod/valibot build the schema as the source of truth and derive the type. Same destination — runtime validation + static types from one definition — opposite starting points. Pydantic v2 pushed validation into a Rust core (pydantic-core, 5-50x faster than v1), which is its equivalent of zod’s v4 performance rewrite: the incumbents in both languages answered their speed critics the same way.
Positioning across the trio: zod = TS ecosystem gravity, valibot = TS minimal-bundle discipline, pydantic = Python’s annotation-first incumbent (no serious Python rival — msgspec competes on speed only). Choosing per language is mostly decided already: TS server code → zod, TS client → valibot, Python → pydantic; the choice only reopens when schemas must cross language boundaries, where JSON Schema export (pydantic, zod v4 both emit it) becomes the interchange format.
Related: Golden repo (schema validation as a repo rule — validate at boundaries) · zod (OSS) · valibot (OSS) · pydantic (OSS)