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 bundle

Measured (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

Axiszod (OSS)valibot (OSS)
API styleobject methods, .min(), .default()function pipes, pipe(x, y)
Version4.6.51.5.0
Stars44.0k9.0k
Weekly npm downloads~341M~23M
Bundle (gzip, full)88.6 KB14.7 KB
Tree-shakingpartialthe design goal (< 0.7 KB minimal)
Ecosystemthe de-facto standard: tRPC, React Hook Form, OpenAPI tooling, LLM structured-output libs all assume itadapters exist for most, but Zod is the default assumption
Errors.safeParse → single ZodError with path arraysafeParse → issues[] (multiple by design)
Custom messages.min(1, "required") inlinev.minLength(1, "required") inline — same shape
Docs/maturityv4 rewrite shipped (faster, smaller than v3), huge cookbookv1 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 needed

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