zod vs joi vs yup vs superstruct vs io-ts vs runtypes
Data Validation and Type Safety Comparison
1 Year
zodjoiyupsuperstructio-tsruntypesSimilar Packages:
What's Data Validation and Type Safety?

Data validation and type safety libraries in JavaScript help ensure that the data your application processes meets specific criteria. These libraries provide tools to define schemas, validate data against those schemas, and enforce type safety, which can prevent runtime errors and improve code reliability. They are particularly useful in applications that handle user input, API responses, or any data that needs to be validated before processing. Libraries like joi, yup, and zod offer intuitive APIs for defining validation rules, while io-ts and runtypes focus on integrating type safety with runtime validation. superstruct provides a simple and composable way to define structures and validate data, making it easy to create reusable validation logic.

Package Weekly Downloads Trend
Github Stars Ranking
Stat Detail
Package
Downloads
Stars
Size
Issues
Publish
License
zod31,580,26138,4131.62 MB59619 hours agoMIT
joi12,287,15321,107531 kB190a year agoBSD-3-Clause
yup7,573,75123,394260 kB2466 months agoMIT
superstruct2,770,4767,121182 kB98a year agoMIT
io-ts2,005,7086,779460 kB1646 months agoMIT
runtypes190,0962,654312 kB204 months agoMIT
Feature Comparison: zod vs joi vs yup vs superstruct vs io-ts vs runtypes

TypeScript Integration

  • zod:

    zod is a TypeScript-first library that provides excellent type inference out of the box. It is designed to work seamlessly with TypeScript, making it easy to define schemas and extract types with minimal effort.

  • joi:

    joi has basic TypeScript support, but its dynamic nature means that type information is not always preserved in a way that TypeScript can fully leverage. This can lead to some limitations when trying to infer types directly from joi schemas.

  • yup:

    yup provides decent TypeScript support, especially for its API methods. However, like joi, it is not fully type-safe, and some type information may be lost during validation, which can lead to challenges in type inference.

  • superstruct:

    superstruct offers good TypeScript support, allowing you to define types alongside your validations. However, it is not as type-centric as some other libraries, which may limit its usefulness in highly type-driven projects.

  • io-ts:

    io-ts provides strong TypeScript integration by defining codecs that encode, decode, and validate data while preserving type information. This allows for seamless integration of runtime validation with static type checking, making it ideal for TypeScript projects.

  • runtypes:

    runtypes is designed with TypeScript in mind, providing clear type definitions and allowing for easy extraction of types from runtype validators. This makes it a great choice for projects that prioritize type safety and clarity.

Asynchronous Validation

  • zod:

    zod supports asynchronous validation by allowing schema methods to return promises. It provides a straightforward API for handling async validation, making it suitable for modern applications that require non-blocking operations.

  • joi:

    joi is well-known for its robust support for asynchronous validation, allowing schemas to validate values using async functions. This makes it ideal for scenarios where validation depends on external data or requires non-blocking operations.

  • yup:

    yup is designed with asynchronous validation in mind, allowing schema methods to return promises. This makes it particularly suitable for form validation scenarios where async checks (e.g., email uniqueness) are required.

  • superstruct:

    superstruct supports asynchronous validation by allowing validators to return promises. This feature makes it flexible for use cases where validation may involve async operations, such as checking values against a database.

  • io-ts:

    io-ts supports asynchronous validation through its codec API, but it is not the primary focus of the library. Developers can implement async validation by leveraging promises within custom codecs.

  • runtypes:

    runtypes primarily focuses on synchronous validation. While it is possible to implement asynchronous validation with runtypes, it does not provide built-in support for async validation out of the box.

Complex Validation

  • zod:

    zod supports complex validation through its composable schema design, allowing for nested structures, unions, intersections, and custom validations. It provides a clear and concise API for defining intricate validation logic while maintaining strong type safety.

  • joi:

    joi is one of the best libraries for complex validation, offering a rich set of features for defining intricate validation rules, including nested objects, conditional validations, and custom validators. Its expressive API makes it easy to handle even the most challenging validation requirements.

  • yup:

    yup is highly capable of handling complex validations, including nested objects, arrays, and conditional validations. Its schema-based approach allows for clear and organized definitions of intricate validation rules, making it a popular choice for form validation in React applications.

  • superstruct:

    superstruct allows for complex validations by composing multiple structs and creating custom validators. Its simple and intuitive API makes it easy to build and manage complex validation logic without becoming overly complicated.

  • io-ts:

    io-ts excels at complex validation scenarios, especially those that require custom codecs and combinators. Its functional programming approach allows for highly composable and reusable validation logic.

  • runtypes:

    runtypes supports complex validation through the use of custom runtimes and combinators, but it is more lightweight compared to joi and io-ts. It is well-suited for projects that need clear and concise validations without excessive complexity.

Ease of Use: Code Examples

  • zod:

    zod Example

    import { z } from 'zod';
    
    const UserSchema = z.object({
      name: z.string(),
      age: z.number().min(0),
    });
    
    const result = UserSchema.safeParse({ name: 'Alice', age: 30 });
    
    if (result.success) {
      console.log('Valid:', result.data);
    } else {
      console.error('Validation Error:', result.error);
    }
    
  • joi:

    joi Example

    const Joi = require('joi');
    
    const schema = Joi.object({
      name: Joi.string().required(),
      age: Joi.number().integer().min(0).required(),
    });
    
    const { error, value } = schema.validate({ name: 'Alice', age: 30 });
    
    if (error) {
      console.error('Validation Error:', error.details);
    } else {
      console.log('Valid Value:', value);
    }
    
  • yup:

    yup Example

    const yup = require('yup');
    
    const schema = yup.object().shape({
      name: yup.string().required(),
      age: yup.number().positive().integer().required(),
    });
    
    schema.validate({ name: 'Alice', age: 30 })
      .then((value) => console.log('Valid:', value))
      .catch((err) => console.error('Validation Error:', err));
    
  • superstruct:

    superstruct Example

    import { struct } from 'superstruct';
    
    const User = struct({
      name: 'string',
      age: 'number',
    });
    
    const result = User({ name: 'Alice', age: 30 });
    console.log('Validated:', result);
    
  • io-ts:

    io-ts Example

    import * as t from 'io-ts';
    
    const User = t.type({
      name: t.string,
      age: t.number,
    });
    
    const result = User.decode({ name: 'Alice', age: 30 });
    
    if (result._tag === 'Right') {
      console.log('Valid:', result.right);
    } else {
      console.log('Invalid:', result.left);
    }
    
  • runtypes:

    runtypes Example

    import { Runtype, String, Number, Record } from 'runtypes';
    
    const User = Record({
      name: String,
      age: Number,
    });
    
    const result = User.check({ name: 'Alice', age: 30 });
    console.log('Validated:', result);
    
How to Choose: zod vs joi vs yup vs superstruct vs io-ts vs runtypes
  • zod:

    Choose zod if you want a TypeScript-first schema declaration and validation library that emphasizes type inference and provides a simple, intuitive API. It is designed for modern TypeScript projects and offers excellent developer experience with minimal boilerplate.

  • joi:

    Select joi if you require a powerful and flexible schema description language for validating JavaScript objects. It is well-suited for complex validations, supports asynchronous validation, and is widely used in the Node.js ecosystem, making it a reliable choice for server-side applications.

  • yup:

    Select yup if you need a schema builder for value parsing and validation that is inspired by joi but is more focused on working with promises and asynchronous validation. It is a great choice for form validation in React applications due to its integration with libraries like Formik.

  • superstruct:

    Choose superstruct if you prefer a minimalistic and composable approach to data validation. It allows you to define structures using simple functions, making it easy to create reusable and maintainable validation logic without a steep learning curve.

  • io-ts:

    Choose io-ts if you need a library that integrates seamlessly with TypeScript to provide both runtime validation and static type checking. It is ideal for projects where type safety is a priority, and you want to leverage TypeScript's capabilities to catch errors early.

  • runtypes:

    Opt for runtypes if you want a lightweight library that combines runtime type checking with a simple API. It is particularly useful for projects that need clear and concise type validations without the overhead of a more complex framework.

README for zod

Zod logo

Zod

TypeScript-first schema validation with static type inference
by @colinhacks


Zod CI status License npm discord server stars

Docs   •   Discord   •   𝕏   •   Bluesky


Featured sponsor: Jazz

jazz logo

Learn more about featured sponsorships




Read the docs →



What is Zod?

Zod is a TypeScript-first validation library. Define a schema and parse some data with it. You'll get back a strongly typed, validated result.

import { z } from "zod/v4";

const User = z.object({
  name: z.string(),
});

// some untrusted data...
const input = {
  /* stuff */
};

// the parsed result is validated and type safe!
const data = User.parse(input);

// so you can use it with confidence :)
console.log(data.name);

Features

  • Zero external dependencies
  • Works in Node.js and all modern browsers
  • Tiny: 2kb core bundle (gzipped)
  • Immutable API: methods return a new instance
  • Concise interface
  • Works with TypeScript and plain JS
  • Built-in JSON Schema conversion
  • Extensive ecosystem

Installation

npm install zod

Basic usage

Before you can do anything else, you need to define a schema. For the purposes of this guide, we'll use a simple object schema.

import { z } from "zod/v4";

const Player = z.object({
  username: z.string(),
  xp: z.number(),
});

Parsing data

Given any Zod schema, use .parse to validate an input. If it's valid, Zod returns a strongly-typed deep clone of the input.

Player.parse({ username: "billie", xp: 100 });
// => returns { username: "billie", xp: 100 }

Note — If your schema uses certain asynchronous APIs like async refinements or transforms, you'll need to use the .parseAsync() method instead.

const schema = z.string().refine(async (val) => val.length <= 8);

await schema.parseAsync("hello");
// => "hello"

Handling errors

When validation fails, the .parse() method will throw a ZodError instance with granular information about the validation issues.

try {
  Player.parse({ username: 42, xp: "100" });
} catch (err) {
  if (error instanceof z.ZodError) {
    err.issues;
    /* [
      {
        expected: 'string',
        code: 'invalid_type',
        path: [ 'username' ],
        message: 'Invalid input: expected string'
      },
      {
        expected: 'number',
        code: 'invalid_type',
        path: [ 'xp' ],
        message: 'Invalid input: expected number'
      }
    ] */
  }
}

To avoid a try/catch block, you can use the .safeParse() method to get back a plain result object containing either the successfully parsed data or a ZodError. The result type is a discriminated union, so you can handle both cases conveniently.

const result = Player.safeParse({ username: 42, xp: "100" });
if (!result.success) {
  result.error; // ZodError instance
} else {
  result.data; // { username: string; xp: number }
}

Note — If your schema uses certain asynchronous APIs like async refinements or transforms, you'll need to use the .safeParseAsync() method instead.

const schema = z.string().refine(async (val) => val.length <= 8);

await schema.safeParseAsync("hello");
// => { success: true; data: "hello" }

Inferring types

Zod infers a static type from your schema definitions. You can extract this type with the z.infer<> utility and use it however you like.

const Player = z.object({
  username: z.string(),
  xp: z.number(),
});

// extract the inferred type
type Player = z.infer<typeof Player>;

// use it in your code
const player: Player = { username: "billie", xp: 100 };

In some cases, the input & output types of a schema can diverge. For instance, the .transform() API can convert the input from one type to another. In these cases, you can extract the input and output types independently:

const mySchema = z.string().transform((val) => val.length);

type MySchemaIn = z.input<typeof mySchema>;
// => string

type MySchemaOut = z.output<typeof mySchema>; // equivalent to z.infer<typeof mySchema>
// number