Runtime validation in TypeScript has never been more contested. For years, Zod was the obvious answer — you installed it, you used it, done. But the landscape shifted hard. Valibot showed up and made Zod look obese. Typia came along and made everything else look slow. Effect/Schema matured and made everyone else look simplistic.
If you’re starting a new project or reviewing your validation stack, this comparison will save you a week of reading docs and Reddit arguments. We’re going to look at real trade-offs, not marketing copy.
The Contenders at a Glance
Before diving deep, here’s the raw summary for people who read tables before text:
| Library | Bundle (minzipped) | Validation speed | Requires build step | Error quality | Learning curve |
|---|---|---|---|---|---|
| Zod v4 | ~8 KB | Good | No | Excellent | Low |
| Valibot v1 | ~1 KB (tree-shaken) | Great | No | Good | Low |
| Typia v6 | ~0 KB runtime | Blazing | Yes (compiler) | Decent | Medium |
| Effect/Schema | ~40 KB (Effect core) | Good | No | Best-in-class | High |
That table alone tells you a lot. Let’s go library by library.
Zod v4 — The Grown-Up Default
GitHub: github.com/colinhacks/zod
Zod v4 dropped in 2025 and addressed the two main complaints against it: bundle size and performance. The internal architecture was rebuilt from scratch. The new z.mini API strips it down further for edge/browser-constrained environments.
import { z } from "zod";
const UserSchema = z.object({
id: z.string().uuid(),
email: z.email(), // new shorthand in v4
age: z.number().int().min(0).max(150),
role: z.enum(["admin", "user", "guest"]),
createdAt: z.iso.datetime(), // new in v4
});
type User = z.infer<typeof UserSchema>;
// Safe parse — never throws
const result = UserSchema.safeParse(rawInput);
if (!result.success) {
console.log(result.error.issues);
// [ { code: 'invalid_string', path: ['email'], message: '...' } ]
}
The .email(), .url(), and .uuid() shorthands replacing .string().email() are small but genuinely pleasant. z.iso.datetime() is a proper RFC 3339 parser, not a regex kludge.
Error messages are Zod’s strongest suit. The issue paths are precise, the codes are consistent, and plugging it into react-hook-form or any form library takes about four lines.
Gotcha: z.transform() and z.preprocess() are still traps for the unwary. If you transform data inside a schema, the inferred type changes — which means z.input<typeof Schema> and z.output<typeof Schema> diverge. This bites you when you use the same schema for both form input validation and API response parsing. Keep your transform schemas separate from your structural schemas.
Gotcha #2: Zod v4 introduced z.mini but it’s a different import path (zod/mini), and not all plugins/integrations support it yet. Don’t switch to zod/mini assuming your toolchain follows.
Production verdict: Still the safest choice for a team that values stability, ecosystem support, and legible errors over extreme performance. The @trpc/server, react-hook-form, drizzle-orm, and next-safe-action integrations are all battle-tested against Zod. It’s the library that has the most "just works" integrations.
Valibot v1 — The Diet Zod That Grew Up
GitHub: github.com/fabian-hiller/valibot
Valibot took a fundamentally different approach to bundle size: instead of shipping a monolithic class-based schema system, every validator is a standalone function. You only pay for what you import.
import * as v from "valibot";
const UserSchema = v.object({
id: v.pipe(v.string(), v.uuid()),
email: v.pipe(v.string(), v.email()),
age: v.pipe(v.number(), v.integer(), v.minValue(0), v.maxValue(150)),
role: v.picklist(["admin", "user", "guest"]),
createdAt: v.pipe(v.string(), v.isoDateTime()),
});
type User = v.InferOutput<typeof UserSchema>;
const result = v.safeParse(UserSchema, rawInput);
if (!result.success) {
console.log(v.flatten(result.issues));
}
The v.pipe() composition model clicks fast once you see it. Actions (.uuid(), .email()) are just functions you pipe into a type validator. The whole API is ~1 KB per schema type imported. For edge functions, serverless with cold-start sensitivity, or browser bundles where every KB matters — Valibot is genuinely hard to beat.
v1 stabilized the async story significantly. v.parseAsync and custom async actions work without surprise, which was a rough edge in earlier versions.
Gotcha: The v.flatten(result.issues) helper exists, but Valibot’s error format is more verbose than Zod’s out of the box. You’ll need to wire up custom error maps if you want user-facing messages that don’t look like an exception dump. The Zod ecosystem of form adapters is larger — many form libraries have a zodResolver but no valibotResolver. This is improving, but check your specific stack.
Gotcha #2: Type inference for complex discriminated unions is still occasionally wrong in Valibot v1 in edge cases. It’s tracked upstream, but if you’re doing complex polymorphic schemas, run your discriminated union types through an explicit test file before committing to Valibot.
Production verdict: The right call for any project where bundle size is a real constraint — not a theoretical one. Serverless edge functions, browser-side validation in a non-bundled context, mobile web with aggressive performance budgets. The API is pleasant enough that you won’t be fighting it daily.
Typia v6 — The Compiler Trick Nobody Warned You About
GitHub: github.com/samchon/typia
Typia is architecturally alien compared to the others. Instead of a runtime schema definition, you write plain TypeScript types — and Typia’s transformer generates the validator code at compile time. Zero schema syntax to learn. Zero runtime overhead from schema construction.
import typia from "typia";
interface User {
id: string & typia.tags.Format<"uuid">;
email: string & typia.tags.Format<"email">;
age: number & typia.tags.Type<"int32"> & typia.tags.Minimum<0>;
role: "admin" | "user" | "guest";
createdAt: string & typia.tags.Format<"date-time">;
}
// This call gets replaced by generated code at build time
const validate = typia.createValidate<User>();
const result = validate(rawInput);
if (!result.success) {
console.log(result.errors);
}
The generated validator for a complex type is tens of thousands of operations per second faster than anything Zod or Valibot can do. On an LLM API response pipeline processing 50k objects per second, this matters.
The tags system (typia.tags.Format<"uuid">) is the escape hatch for semantic constraints. It’s a bit verbose compared to .uuid() chainability, but it keeps your type definitions as the single source of truth.
Gotcha (and it’s a big one): Typia requires either ts-patch or the @typia/unplugin-* family for your bundler. This means it touches your TypeScript compilation step. Getting it running in a monorepo with esbuild + tsc type checking + vitest is non-trivial. Expect to spend a real afternoon on toolchain config the first time. If you’re on Next.js with Turbopack, check current compatibility before committing.
Gotcha #2: The generated code is verbose and not meant to be read. Debugging a failed validation in production via the error output is less ergonomic than Zod. Error messages like "$input.age is not integer" are accurate but raw. Build a formatting layer.
Gotcha #3: Typia doesn’t give you a reusable schema object in the traditional sense. You can’t pass a UserSchema to a form library resolver. The use case is data pipeline validation, not form validation.
Production verdict: Use Typia when you’re validating high-volume structured data — API responses, message queue payloads, database result rows — and you’ve already profiled that validation is a bottleneck. Don’t use it as your form validation layer. Don’t use it in teams where people will be confused by the build-time magic. The performance ceiling is legitimately impressive; the ergonomic ceiling for general use cases is lower than Zod.
Effect/Schema — The Power Tool You Might Not Need Yet
GitHub: github.com/Effect-TS/effect
Effect/Schema (effect/Schema) is part of the Effect ecosystem and takes a fundamentally different philosophy. Where Zod parses data, Effect/Schema both parses and serializes, models bidirectional transformations, and integrates with Effect’s typed error handling and dependency injection.
import { Schema } from "effect";
const User = Schema.Struct({
id: Schema.UUID,
email: Schema.String.pipe(Schema.pattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)),
age: Schema.Int.pipe(Schema.between(0, 150)),
role: Schema.Literal("admin", "user", "guest"),
createdAt: Schema.DateTimeUtc,
});
type User = Schema.Schema.Type<typeof User>;
// Decoding (external → internal)
const result = Schema.decodeUnknownEither(User)(rawInput);
// Encoding (internal → external, e.g., serializing for storage)
const encoded = Schema.encodeEither(User)(userObject);
The decode/encode duality is where Effect/Schema shines. If you have a Date that needs to be stored as an ISO string, you define that transformation once in the schema — and it works bidirectionally, with proper error typing in both directions.
ParseError in Effect is a proper typed error with full path information, causes, and the ability to format it exactly as needed. The error quality is unmatched.
The deep Effect integration means you can build pipelines like:
import { Effect, Schema } from "effect";
const processUser = (raw: unknown) =>
Schema.decodeUnknown(User)(raw).pipe(
Effect.flatMap(saveToDatabase),
Effect.flatMap(sendWelcomeEmail),
Effect.catchTag("ParseError", (e) => logAndReturn400(e)),
);
This is genuinely powerful for complex backend flows.
Gotcha: The bundle cost is real. You’re importing the entire Effect runtime. For a standalone validation use case, the ~40 KB (minzipped) overhead is unjustifiable when Zod does the job for 8 KB. Effect/Schema earns its keep when you’re already in the Effect ecosystem — not as a drop-in Zod replacement.
Gotcha #2: The learning curve is steep. Effect’s paradigm — functional effects, typed errors, layers, services — requires rewiring your mental model of TypeScript development. Don’t introduce Effect/Schema to a team that hasn’t bought into functional-effects-style TypeScript. The resulting code will read like a foreign language to anyone not already familiar, and that’s a maintenance liability.
Gotcha #3: The ecosystem of integrations (form resolvers, tRPC adapters, etc.) is thinner than Zod’s. You’ll build more glue code.
Production verdict: The best choice if you’re already using Effect, or if you’re building a complex data pipeline with bidirectional serialization requirements and typed error handling. Not the right default for "I need to validate an API request." The power is real; so is the cost.
Head-to-Head: What Actually Matters
Bundle Size
If you’re shipping to a browser or edge function, Valibot wins with near-zero tree-shaken cost. Zod v4 is acceptable. Typia generates zero runtime schema overhead. Effect/Schema is only viable if you’re already paying the Effect tax.
Raw Validation Throughput
Typia is in a different league — 10-50x faster than Zod in benchmarks for complex objects. Valibot is roughly 1.5-2x faster than Zod v3 and comparable to Zod v4 in most real-world scenarios. Effect/Schema is in the Zod range. For most applications, none of this matters. For a service validating 100k+ objects per second, Typia is worth the toolchain cost.
Developer Experience
Zod v4 wins on DX for most developers. The method-chaining API is intuitive, the error format is useful, the ecosystem is unmatched. Valibot’s pipe model takes twenty minutes to internalize and then feels natural. Typia requires zero schema learning (types are the schema) but costs you in toolchain complexity. Effect/Schema is powerful but demands commitment to the Effect paradigm.
Error Messages for End Users
Effect/Schema > Zod > Valibot > Typia. If you’re surfacing validation errors to humans (form errors, API error responses), Zod and Effect/Schema save you the most work. Typia in particular requires a custom formatting layer.
The Decision Framework
Pick Zod v4 if:
- You want the path of least resistance
- Your stack includes tRPC, react-hook-form, drizzle, or any ecosystem library with a Zod adapter
- You need rich error formatting for user-facing validation
- Your team is varied in TypeScript experience
Pick Valibot if:
- Bundle size is a real constraint, not a theoretical preference
- You’re building an edge function or browser-side validator
- You’re comfortable writing your own form adapter glue
- Your schemas aren’t deeply complex (discriminated unions at scale, tread carefully)
Pick Typia if:
- You’re validating high-volume structured data (API responses, queue messages)
- You’ve already profiled and validation is a measurable bottleneck
- You have control over the build pipeline and can absorb the
ts-patchcomplexity - You don’t need form integration
Pick Effect/Schema if:
- You’re already using Effect
- You need true bidirectional encode/decode
- You want typed errors that compose into larger Effect pipelines
- Your team is Effect-fluent
Don’t use two of these in the same project unless you have a very good reason. The schema types don’t compose across libraries, and you’ll end up with two incompatible validation philosophies in your codebase. Pick one and standardize.
One More Real-World Observation
The "Zod is slow" narrative from 2023-2024 is largely obsolete with v4. Unless you’re actually hitting performance walls, don’t pre-optimize your validation library. The bigger risk for most teams is picking a library with poor error ergonomics and spending engineering hours on custom error formatters — which is where Typia’s otherwise excellent performance story often breaks down in practice.
The right call in 2026 for the vast majority of TypeScript projects: Zod v4. For bundle-sensitive workloads: Valibot. For data-pipeline performance: Typia. For Effect shops: Effect/Schema. The competition is healthy and has forced every library to improve. That’s a win for everyone.