Three runtime validation libraries compared directly
Anyone choosing runtime validation for TypeScript faces three very different philosophies: io-ts with a functional fp-ts ecosystem, Zod with a fluent, object oriented API and a broad ecosystem, and Valibot with a modular, tree-shakeable architecture for minimal bundle sizes. This article compares all three based on API style, error handling, bundle cost and migration effort, so the choice fits the project architecture instead of following a trend.
Table of Contents
- 1. Three libraries, three philosophies of runtime validation
- 2. io-ts: Functional validation in the fp-ts ecosystem
- 3. Zod: Fluent API and a broad ecosystem
- 4. Valibot: Modular functions and tree-shaking
- 5. Error handling compared: Either, ZodError and issues
- 6. Bundle size and performance in practice
- 7. Ecosystem integration: forms, RPC and codegen
- 8. Assessing migration effort between the libraries
- 9. io-ts, Zod and Valibot compared directly
- 10. Summary
- 11. FAQ
1. Three libraries, three philosophies of runtime validation
All three libraries solve the same underlying problem: TypeScript types exist only at compile time, so external data from forms, APIs, or configuration files needs a real runtime check before it can be trusted. But io-ts, Zod, and Valibot differ fundamentally in how this check is modeled, functionally with explicit Either values, object oriented with a chained fluent API, or modularly with individually importable functions.
This distinction is not merely a matter of taste. A team already working with fp-ts benefits from io-ts' consistent functional integration. A team without a functional background often finds a more direct entry point in Zod's fluent API. And a project with strict bundle size requirements, such as a performance critical storefront, gains the most from Valibot's consistent tree-shaking. The following sections place these three philosophies in context using concrete code examples.
2. io-ts: Functional validation in the fp-ts ecosystem
io-ts was one of the first TypeScript libraries to couple runtime validation and type inference from a single source, long before Zod popularized this concept. The decisive difference from the other two libraries lies in the return value: io-ts' decode() function does not return a result object with a success flag, it returns an Either type from the fp-ts ecosystem, containing either a Left with validation errors or a Right with the validated value.
This coupling to fp-ts is both a strength and an entry barrier. Anyone already working with pipe(), fold(), and functional error handling fits io-ts seamlessly into existing patterns. Without fp-ts experience on the team, handling Either feels unfamiliar at first compared to a simple if (!result.success), which in practice makes io-ts a choice mainly for teams that have already established functional programming as an architecture principle.
import * as t from "io-ts";
import { pipe } from "fp-ts/function";
import { fold } from "fp-ts/Either";
// io-ts codec: runtime check and static type from one definition
const UserCodec = t.type({
id: t.string,
email: t.string,
age: t.number,
});
type User = t.TypeOf<typeof UserCodec>;
function decodeUser(input: unknown): void {
// decode() returns an Either<Errors, User>, not a plain result object
const result = UserCodec.decode(input);
pipe(
result,
fold(
(errors) => console.error(`Invalid user: ${errors.length} issues`),
(user: User) => console.log(`Valid user: ${user.email}`)
)
);
}
3. Zod: Fluent API and a broad ecosystem
Zod relies on a chained, object oriented API that does not require any additional functional library. Methods like .min(), .email(), or .optional() are attached directly to a base schema, and .safeParse() returns a plain result object with a success flag, a pattern immediately understandable to most TypeScript developers without a functional background.
The decisive advantage of Zod lies in its ecosystem: React Hook Form, tRPC, tanStack Form, and numerous other libraries offer native Zod integrations, so a Zod schema often handles not just validation but also type inference for entire form or RPC layers. This wide adoption frequently makes Zod the default choice for new projects, regardless of whether io-ts or Valibot would be technically superior.
import { z } from "zod";
// Zod: fluent, chainable API, no separate functional library required
const userSchema = z.object({
id: z.string(),
email: z.string().email(),
age: z.number().int().positive(),
});
type User = z.infer<typeof userSchema>;
function decodeUser(input: unknown): void {
// safeParse returns a plain result object with a "success" flag
const result = userSchema.safeParse(input);
if (!result.success) {
console.error(`Invalid user: ${result.error.issues.length} issues`);
return;
}
console.log(`Valid user: ${result.data.email}`);
}
4. Valibot: Modular functions and tree-shaking
Valibot is the youngest of the three libraries and was designed specifically as an answer to Zod's bundle size problem. Instead of a central z class with coupled methods, Valibot imports every validation function individually, string(), email(), minLength(), connected through a functional pipe() syntax. A bundler can then consistently remove every validator that a project does not actually use.
This architectural decision makes Valibot the preferred choice for performance critical frontends with a strict JavaScript budget, such as in a Hyvä context. The trade-off: the ecosystem of third party integrations is younger and smaller than Zod's, and teams more often have to build their own adapters when integrations are missing, an effort that has to be weighed against the saved bundle size.
import * as v from "valibot";
// Valibot: individual, tree-shakeable functions via a functional pipe
const UserSchema = v.object({
id: v.string(),
email: v.pipe(v.string(), v.email()),
age: v.pipe(v.number(), v.integer(), v.minValue(1)),
});
type User = v.InferOutput<typeof UserSchema>;
function decodeUser(input: unknown): void {
// safeParse mirrors Zod's result shape, but only imports used validators
const result = v.safeParse(UserSchema, input);
if (!result.success) {
console.error(`Invalid user: ${result.issues.length} issues`);
return;
}
console.log(`Valid user: ${result.output.email}`);
}
5. Error handling compared: Either, ZodError and issues
The error objects of the three libraries differ structurally quite a bit. io-ts returns an array of ValidationError entries inside the Left branch of an Either, each with a context path that describes the position within the nested type, but without the directly readable error message familiar from Zod. This raw form usually has to be turned into readable text first, using a helper such as PathReporter.report().
Zod's ZodError offers two built in methods, .flatten() and .format(), that convert errors directly into field keyed, UI friendly structures, a clear convenience advantage over io-ts. Valibot's issues array is conceptually closer to Zod than to io-ts, with flat objects per error containing path and message, but without a built in helper equivalent to Zod's flatten(), so teams more often write a small mapping function of their own here.
6. Bundle size and performance in practice
For a typical form schema with a dozen fields, Zod usually sits somewhere between ten and fourteen kilobytes minified and compressed, regardless of how many of the included methods are actually used, since the core class is imported as a single connected block. Valibot often reaches only one to three kilobytes for the same schema, because only the validation functions actually used end up in the bundle.
io-ts falls somewhere between the two in terms of bundle cost, depending on how much of the fp-ts ecosystem a project already loads anyway. For a project that already uses fp-ts for state management or asynchronous data flows, io-ts adds barely any extra cost. For a project without an existing fp-ts dependency, io-ts brings the entire functional foundation along as additional weight, which can push the bundle size notably above Zod and Valibot.
7. Ecosystem integration: forms, RPC and codegen
Zod's market share shows up most clearly in the number of third party integrations: React Hook Form, tRPC, tanStack Router, and tanStack Form all offer native Zod adapters, so a single schema types form validation, API contract, and route parameters at the same time. These network effects frequently make Zod the most pragmatic choice, even when Valibot would be the technically lighter option.
Valibot's ecosystem is growing, now that tanStack Form and some RPC libraries offer Valibot adapters alongside their Zod adapters, but it remains smaller overall. io-ts is most tightly interwoven with the fp-ts ecosystem itself, for example with io-ts-types for additional codecs, but barely integrated with form libraries outside the functional world, which makes io-ts more of an internal validation layer than a direct form binding tool.
8. Assessing migration effort between the libraries
A migration from Zod to Valibot primarily affects syntax, not structure: z.object({ email: z.string().email() }) becomes v.object({ email: v.pipe(v.string(), v.email()) }), a mechanical, often scriptable rewrite that leaves the schema structure unchanged. The derived type only switches from z.infer to v.InferOutput.
A migration between io-ts and Zod or Valibot is more involved, because it changes the entire error handling style, from Either based pattern matching to a simple if (!result.success). Every location in the code that applies fold() or pipe() to a validation result needs to be rewritten, which is why such a switch usually happens incrementally, module by module, instead of in one large refactoring.
9. io-ts, Zod and Valibot compared directly
The following table summarizes the key differences and maps each library to a typical usage scenario.
| Criterion | io-ts | Zod | Valibot |
|---|---|---|---|
| API style | Functional, Either based | Fluent, chained methods | Modular, pipe based |
| Bundle size (typical) | Depends on fp-ts usage | ~10 to 14 KB | ~1 to 3 KB |
| Ecosystem | fp-ts world | Very broad (RHF, tRPC, tanStack) | Growing, smaller than Zod |
| Entry barrier | High without fp-ts experience | Low | Low to moderate |
| Error objects | Either errors, reporter needed | flatten()/format() built in | issues array, own mapping often needed |
| Typical use | fp-ts projects, backend domain logic | General purpose, admin tools, full stack | Performance critical storefronts |
None of the three libraries is superior in every category, the choice depends on which criterion weighs the most for a concrete project, functional consistency, ecosystem breadth, or bundle size.
Mironsoft
Choosing, migrating and integrating a validation library into your architecture
Find the right runtime validation for your project?
We analyze your stack, bundle budget and ecosystem needs and give a well founded recommendation between io-ts, Zod and Valibot, including a migration plan for existing schemas.
Technology selection
Well founded recommendation based on bundle budget and ecosystem
Migration
Incremental rebuild of existing schemas without a big bang risk
Performance audit
Measuring bundle size and optimizing validation code specifically
10. Summary
The comparison between io-ts, Zod and Valibot shows three different answers to the same question, how to derive runtime validation and TypeScript types from a single source. io-ts fits best for teams that already work consistently with fp-ts and have established Either based error handling as their standard. Zod offers the broadest ecosystem integration and the lowest entry barrier, which keeps it the pragmatic default choice for most general purpose projects.
Valibot wins wherever bundle size matters measurably, especially in performance critical frontends with a strict JavaScript budget, such as in a Hyvä context. A migration between Zod and Valibot is usually mechanical and low risk, while switching between io-ts and the other two libraries affects the entire error handling style and should be planned incrementally accordingly.
io-ts vs. Zod vs. Valibot, the key points at a glance
io-ts
Functional, Either based, best choice with an existing fp-ts ecosystem.
Zod
Fluent API, broadest ecosystem, pragmatic default for general purpose projects.
Valibot
Tree-shakeable, minimal bundle size, ideal for performance critical storefronts.
Migration
Zod/Valibot mechanical, io-ts requires a complete change in error handling style.