DEV Community

Carlos Oliva Pascual
Carlos Oliva Pascual

Posted on Originally published at stacknotice.com

Zod vs Valibot (2026): When Bundle Size Actually Matters for Validation

Both Zod and Valibot solve the same problem: TypeScript types only exist at compile time, but you need to validate unknown data at runtime — API inputs, form data, environment variables. Both generate TypeScript types from schemas. Both work with tRPC, Hono, and React Hook Form. The differences are bundle size, API design, and ecosystem maturity.

The Bundle Gap

Library Minified + Gzipped Tree-shaking
Zod v3 ~55kb Limited
Valibot v1 ~10-12kb total, ~1-2kb per schema Full

For Node.js APIs, irrelevant. For Cloudflare Workers or browser bundles, it matters.

API: Method Chaining vs Function Pipeline

// Zod — chained methods
const CreateUserSchema = z.object({
  name: z.string().min(1).max(100),
  email: z.string().email(),
  role: z.enum(['USER', 'ADMIN']).default('USER'),
})
type CreateUser = z.infer<typeof CreateUserSchema>

const result = CreateUserSchema.safeParse(req.body)
if (!result.success) {
  const errors = result.error.flatten().fieldErrors
}

// Valibot — functional pipe()
import { object, string, pipe, minLength, maxLength, email, picklist, optional } from 'valibot'
const CreateUserSchema = object({
  name: pipe(string(), minLength(1), maxLength(100)),
  email: pipe(string(), email()),
  role: optional(picklist(['USER', 'ADMIN']), 'USER'),
})
type CreateUser = InferOutput<typeof CreateUserSchema>

const result = safeParse(CreateUserSchema, req.body)
if (!result.success) {
  const issues = result.issues  // Array<{ message, path }>
}
Enter fullscreen mode Exit fullscreen mode

Zod reads more naturally. Valibot is fully tree-shakeable — bundlers drop functions you don't import.

Transforms and Coercion

// Query param coercion — Zod
const PaginationSchema = z.object({
  page: z.coerce.number().int().min(1).default(1),
  limit: z.coerce.number().int().min(1).max(100).default(20)
})

// Valibot equivalent
const PaginationSchema = object({
  page: optional(pipe(coerce(number(), Number), integer(), minValue(1)), 1),
  limit: optional(pipe(coerce(number(), Number), integer(), minValue(1), maxValue(100)), 20)
})
Enter fullscreen mode Exit fullscreen mode

i18n Error Messages

Valibot's setGlobalConfig gives you a single translation layer for the whole app:

import { setGlobalConfig } from 'valibot'

setGlobalConfig({
  message: (issue) => {
    if (issue.type === 'min_length') return `Mínimo ${issue.requirement} caracteres`
    if (issue.type === 'email') return 'Email inválido'
    return 'Campo inválido'
  }
})
// Every schema picks this up automatically
Enter fullscreen mode Exit fullscreen mode

Framework Integration

Both work with the same ecosystem:

// React Hook Form
import { zodResolver } from '@hookform/resolvers/zod'
import { valibotResolver } from '@hookform/resolvers/valibot'

// Same API, different resolver

// tRPC — Valibot supported since v11
.input(object({ name: string(), email: pipe(string(), email()) }))

// Hono
import { vValidator } from '@hono/valibot-validator'
app.post('/users', vValidator('json', schema), async (c) => { ... })
Enter fullscreen mode Exit fullscreen mode

Decision Framework

Situation Choose
Existing codebase, Node.js API Zod
Cloudflare Workers, edge functions Valibot
Browser bundle, size-sensitive app Valibot
Need full i18n error messages Valibot
Ecosystem maturity matters Zod

They're interchangeable for most use cases. Migrating is a find-and-replace-plus-refactor, not a rearchitecture.


Full article at stacknotice.com/blog/zod-vs-valibot-2026

Top comments (0)