Skip to main content

Zod v4 vs Valibot: The TypeScript Validation Battle in 2026

·PkgPulse Team

TL;DR

Zod v4 closed the bundle-size gap that gave Valibot its initial edge. Zod v4 ships ~1.8KB (down from ~13KB in v3) and adds first-class async validation, pipe transforms, and improved error messages. Valibot still wins on tree-shaking granularity — you only bundle the exact validators you use. For most TypeScript projects: Zod v4 is the pragmatic default with the larger ecosystem. Valibot is the better choice when bundle size is critical (edge functions, CDN scripts) or when you need per-validator granularity.

Key Takeaways

  • Bundle size: Zod v4 ~1.8KB (down from ~13KB); Valibot ~0.3KB base + per-validator
  • Ecosystem: Zod has 20M+ weekly downloads and integrates with every TypeScript library; Valibot ~500K
  • API style: Zod is class-based chains; Valibot is functional/modular
  • Error customization: both excellent, Valibot slightly more granular
  • Async validation: Zod v4 improved significantly; Valibot had it from day one

The Bundle Size Story

Zod v3 (the old baseline):
  Full bundle: ~13KB gzipped
  Even if you use one validator: ~13KB
  Tree-shaking: minimal (class-based, most code needed)

Valibot v0.x (the challenger):
  Base: ~300 bytes
  Each validator adds ~100-300 bytes
  Use z.string().email(): ~600 bytes total
  Use 10 validators: ~3KB total
  Perfect tree-shaking by design

Zod v4 (2025):
  Core: ~1.8KB gzipped
  Full schema: ~4KB
  Improved tree-shaking (not as granular as Valibot, but much better)

Real-world impact:
→ Edge function (Cloudflare Worker): Valibot still wins (0.5KB vs 2KB)
→ Next.js app (server only, no bundle concern): Zod v4 fine
→ Browser bundle with 5+ validators: comparable; Valibot marginally better
→ Library author (must minimize deps): Valibot or Zod v4 both viable

API Comparison: Same Goal, Different Style

// ─── Defining a schema ───

// Zod v4 (method chains on class instances):
import { z } from 'zod';

const UserSchema = z.object({
  id: z.string().uuid(),
  name: z.string().min(1).max(100),
  email: z.string().email(),
  age: z.number().int().min(0).max(150).optional(),
  role: z.enum(['admin', 'user', 'viewer']),
  createdAt: z.date(),
});

type User = z.infer<typeof UserSchema>;

// Valibot (functional, each validator is a separate function):
import * as v from 'valibot';

const UserSchema = v.object({
  id: v.pipe(v.string(), v.uuid()),
  name: v.pipe(v.string(), v.minLength(1), v.maxLength(100)),
  email: v.pipe(v.string(), v.email()),
  age: v.optional(v.pipe(v.number(), v.integer(), v.minValue(0), v.maxValue(150))),
  role: v.picklist(['admin', 'user', 'viewer']),
  createdAt: v.date(),
});

type User = v.InferOutput<typeof UserSchema>;

// Key style difference:
// Zod: z.string().min(1).max(100) — method chain
// Valibot: v.pipe(v.string(), v.minLength(1), v.maxLength(100)) — function composition
// Both are TypeScript-safe; it's a preference question

Parsing and Error Handling

// ─── Zod v4 parsing ───
const result = UserSchema.safeParse(rawInput);
if (!result.success) {
  // result.error is a ZodError
  console.log(result.error.format());
  // {
  //   name: { _errors: ["String must contain at least 1 character(s)"] },
  //   email: { _errors: ["Invalid email"] },
  // }

  // Flat errors:
  console.log(result.error.flatten());
  // { fieldErrors: { name: [...], email: [...] }, formErrors: [] }
}

// Zod v4 improved: error maps are customizable globally or per-schema:
const UserSchema = z.object({
  email: z.string().email({ message: "Please enter a valid email address" }),
  age: z.number({ invalid_type_error: "Age must be a number" }).min(18, {
    message: "You must be at least 18 years old",
  }),
});

// ─── Valibot parsing ───
const result = v.safeParse(UserSchema, rawInput);
if (!result.success) {
  // result.issues is an array of ValiError
  for (const issue of result.issues) {
    console.log(issue.path, issue.message);
    // ['email'] "Invalid email"
  }
}

// Valibot error customization (per-validator):
const UserSchema = v.object({
  email: v.pipe(
    v.string('Email is required'),
    v.email('Please enter a valid email address')
  ),
  age: v.pipe(
    v.number('Age must be a number'),
    v.minValue(18, 'You must be at least 18')
  ),
});

// Both approaches work well for form validation.
// Zod's .flatten() is particularly convenient for React Hook Form integration.

Zod v4's New Features

// 1. Pipe transforms (cleaner than .transform().refine() chains):
const CleanedString = z.string().pipe(
  z.string().trim().toLowerCase()
);
// Equivalent but more readable than z.string().transform(s => s.trim().toLowerCase())

// 2. Improved async validation:
const UserSchema = z.object({
  username: z.string().refine(
    async (username) => {
      // Check DB — now first-class in v4
      const exists = await db.user.findUnique({ where: { username } });
      return !exists;
    },
    { message: "Username already taken" }
  ),
});

// Parse with async:
const result = await UserSchema.safeParseAsync(rawInput);

// 3. ZodMiniCompat — alias for smallest possible Zod v4 build:
import { z } from 'zod/mini'; // ~1.2KB — subset of Zod with most-used validators

// 4. Better .describe() for schema documentation:
const ApiKeySchema = z.string()
  .describe("API key for authentication")
  .min(32)
  .max(64)
  .regex(/^sk_/);
// Used by OpenAPI generators, JSON Schema output, LLM tool definitions

// 5. First-class JSON Schema output:
import { zodToJsonSchema } from 'zod-to-json-schema'; // v4 compatible
const jsonSchema = zodToJsonSchema(UserSchema);
// Used for: OpenAPI spec generation, form generation, LLM tool descriptions

Valibot's Unique Strengths

// 1. Async validation as a first-class concept:
const UserSchema = v.objectAsync({
  username: v.pipeAsync(
    v.string(),
    v.checkAsync(async (username) => {
      const exists = await db.user.findUnique({ where: { username } });
      return !exists;
    }, 'Username already taken')
  ),
});

// 2. Transformations are explicit in the type:
// Valibot distinguishes InferInput vs InferOutput:

const ParsedDateSchema = v.pipe(
  v.string(),       // Input: string
  v.isoDate(),      // Validates ISO date format
  v.transform(s => new Date(s))  // Output: Date
);

type DateInput = v.InferInput<typeof ParsedDateSchema>;   // string
type DateOutput = v.InferOutput<typeof ParsedDateSchema>; // Date

// Zod has this but Valibot makes it more explicit in the type system

// 3. Custom validation actions (composable):
const noSwearWords = v.check<string>(
  (value) => !containsSwearWords(value),
  'This field contains inappropriate language'
);

const PostSchema = v.object({
  title: v.pipe(v.string(), v.minLength(1), noSwearWords),
  body: v.pipe(v.string(), v.minLength(10), noSwearWords),
  // noSwearWords reused across fields without duplication
});

// 4. Smaller per-validator impact:
// Only importing what you use means unused validators = 0KB
// Ideal for: edge functions, browser-side validation in small bundles

Ecosystem Integration

Zod v4 ecosystem (2026):
→ React Hook Form: @hookform/resolvers/zod ✅
→ tRPC: built-in Zod support ✅
→ Prisma: generate Zod schemas from schema ✅
→ Next.js Server Actions: native Zod integration in examples ✅
→ Fastify: fastify-type-provider-zod ✅
→ OpenAPI: zod-openapi, @asteasolutions/zod-to-openapi ✅
→ LLM tool definitions: AI SDK uses Zod natively ✅
→ Hono: @hono/zod-validator ✅

Valibot ecosystem (smaller but growing):
→ React Hook Form: @hookform/resolvers/valibot ✅
→ Modular Valibot: ✅
→ tRPC: community support ✅ (not first-class)
→ Most other integrations: ⚠ check for valibot support

The ecosystem gap is real.
Zod is the default for TypeScript validation — every library supports it.
Valibot requires checking compatibility with each tool you use.

When to Use Which

Use Zod v4 when:
→ You're integrating with tRPC, React Hook Form, Prisma, or AI SDK
→ Team already knows Zod — migration cost isn't worth it
→ Bundle size isn't a critical constraint (server-side apps, larger bundles)
→ You want maximum ecosystem support and answered questions online
→ You're defining API schemas for OpenAPI/LLM tool definitions

Use Valibot when:
→ Bundle size is critical: Cloudflare Workers, browser-side validation, CDN scripts
→ You're starting fresh with no existing validation library
→ You prefer functional composition over method chains
→ You need extremely granular control over which validators are included
→ You're building a library and want minimal peer dependencies

The honest 2026 take:
→ Zod v4's size reduction removed Valibot's biggest advantage
→ Valibot is still technically smaller per-validator
→ But Zod's ecosystem dominance is a real, practical advantage
→ New project on Next.js/tRPC? Default to Zod v4.
→ New project on edge runtime with size constraints? Evaluate Valibot seriously.

Compare Zod, Valibot, and other validation library trends at PkgPulse.

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.