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
| tRPC | Hono | OpenAPI | drizzle-zod | React Hook Form | |
|---|---|---|---|---|---|
| Zod | ✅ | ✅ | ✅ | ✅ (zod-specific) | ✅ |
| Valibot | ✅ (recent) | ✅ | partial | drizzle-valibot | ✅ |
| Effect Schema | partial | partial | partial | no | partial |
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
| Scenario | Pick |
|---|---|
| New TS backend | Zod (or Valibot if bundle-conscious) |
| New TS frontend with bundle constraints | Valibot |
| Existing Zod codebase | stay on Zod |
| All-in on Effect-TS | Effect Schema |
| Need broadest library compat | Zod |
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
- Modern TypeScript Backend with Hono on Bun
- Effect-TS in 2026
- Drizzle ORM Deep Dive
- TypeScript Strict Mode in 2026
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 .