TypeScript validation has three serious options in 2026. Each fits a niche. This post is the comparison.

The contenders

Zod

import { z } from "zod";

const User = z.object({
  email: z.string().email(),
  name: z.string().min(1).max(120),
  age: z.number().int().positive().optional(),
});

type User = z.infer<typeof User>;
const parsed = User.parse(unknownData);

The default. Mature ecosystem. Every library that takes “a validator” supports Zod.

Valibot

import { object, string, email, minLength, maxLength, number, integer, optional, parse } from "valibot";

const User = object({
  email: string([email()]),
  name: string([minLength(1), maxLength(120)]),
  age: optional(number([integer(), minValue(0)])),
});

type User = InferOutput<typeof User>;
const parsed = parse(User, unknownData);

Modular: import only what you use. Tree-shakes aggressively. ~10× smaller bundle for simple schemas.

Effect Schema

import { Schema as S } from "effect";

const User = S.Struct({
  email: S.String.pipe(S.pattern(/^[^@]+@[^@]+$/)),
  name: S.String.pipe(S.minLength(1), S.maxLength(120)),
  age: S.optional(S.Number.pipe(S.int(), S.positive())),
});

type User = S.Schema.Type<typeof User>;
const parsed = S.decodeSync(User)(unknownData);

For Effect-TS shops. Composes with Effects, errors, dependencies.

Bundle size

For a typical schema (10 fields, 20 validators):

Bundle (gzipped)
Zod~13 KB
Valibot~1–3 KB (tree-shaken)
Effect Schema~30 KB (Effect runtime overhead)

For browser bundles, Valibot wins. For backend, doesn’t matter.

Performance

For 10k validations:

  • Valibot: ~5ms.
  • Zod: ~10ms.
  • Effect Schema: ~12ms.

Real apps: validation is rarely the bottleneck. Don’t switch for perf alone.

Ecosystem

tRPCHonoOpenAPIdrizzle-zodReact Hook Form
Zod✅ (zod-specific)
Valibot✅ (recent)partialdrizzle-valibot
Effect Schemapartialpartialpartialnopartial

Zod is the safest bet for ecosystem. Valibot is catching up. Effect Schema only makes sense if you’re going Effect-TS-all-in.

Migration

Valibot’s API is functional / modular vs Zod’s chained. Migration is mechanical but not automatic:

// Zod
z.string().email().min(5)

// Valibot
string([email(), minLength(5)])

Migration script: search-and-replace + manual review. For a 50-schema codebase, 1–2 days.

What I’d pick today

ScenarioPick
New TS backendZod (or Valibot if bundle-conscious)
New TS frontend with bundle constraintsValibot
Existing Zod codebasestay on Zod
All-in on Effect-TSEffect Schema
Need broadest library compatZod

Patterns that work across all three

Parse at boundaries

app.post("/users", async (c) => {
  const payload = User.parse(await c.req.json());  // throws on invalid
  ...
});

Validate every external input. After the boundary, types are trusted.

Derive types

type User = z.infer<typeof User>;        // Zod
type User = InferOutput<typeof User>;    // Valibot

Single source of truth. Schema and type stay in sync.

Reuse + extend

const UserBase = z.object({ email: z.string().email() });
const UserCreate = UserBase.extend({ password: z.string().min(8) });
const UserOut = UserBase.extend({ id: z.number(), createdAt: z.date() });

Share base; specialize. Same in Valibot via merge().

Read this next

If you want a Hono + Zod or Hono + Valibot starter, it’s at rajpoot.dev .


Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .