io-ts vs. Zod vs. Valibot: Runtime Validation Compared
AI generated
<T>
type
TypeScript · io-ts · Zod · Valibot
io-ts vs. Zod vs. Valibot
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.

12 min read io-ts · Zod · Valibot · fp-ts Library comparison · migration

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.

11. FAQ: io-ts vs. Zod vs. Valibot

1What fundamentally distinguishes io-ts from Zod and Valibot?
io-ts returns an Either type from fp-ts instead of a success flag. Fits functional error handling but requires fp-ts knowledge on the team.
2Why is Zod often the default despite a larger bundle?
Zod's ecosystem is the broadest: React Hook Form, tRPC and tanStack Form offer native adapters, which tips the scale for most projects.
3How much smaller is Valibot really?
Zod usually sits at ten to fourteen kilobytes, Valibot often only one to three kilobytes thanks to tree-shaking, depending on validators used.
4Which projects benefit from io-ts?
Projects already working with fp-ts. Without an existing dependency, io-ts brings the entire functional foundation as extra weight.
5How do the error objects differ?
io-ts via PathReporter, Zod with built in flatten()/format(), Valibot's issues array is similar to Zod but lacks an equivalent helper.
6How much effort is a migration from Zod to Valibot?
Mostly mechanical: fluent API calls are translated into pipe() syntax, structure stays the same. z.infer is replaced by v.InferOutput.
7Why is io-ts to Zod migration more involved?
Because the entire error handling style changes, from Either with fold() to plain if checks. Every affected location must be adjusted individually.
8Use several libraries in parallel in one project?
Technically possible, but not advisable for new schemas. During a migration transition, running them in parallel is common and reasonable.
9Which library fits best for Hyvä frontends?
Valibot, since Hyvä favors minimal JavaScript and tree-shaking reduces validation cost to a few kilobytes.
10Is the choice purely a matter of taste?
No, bundle budget, ecosystem and team experience with functional programming are concrete, measurable decision criteria.