Zod vs Yup vs Joi: Which JSON Validator Should You Use in 2026?

Three mature validation libraries, three different design philosophies. Here's when to use Zod, Yup, or Joi — with real performance numbers and migration advice.

·7 min readzodtypescriptvalidationjson

JSON validation is table stakes for any production app. You receive data from an API, an LLM, a webhook, or a user — and you need to know it matches what you expect before you do anything with it. Three libraries dominate the JavaScript ecosystem: Zod, Yup, and Joi. They all solve the same problem, but they make very different trade-offs.

Here's the decision framework.


At a Glance

| | Zod | Yup | Joi | |---|---|---|---| | First released | 2020 | 2014 | 2014 | | Weekly downloads (2026) | ~17M | ~9M | ~6M | | TypeScript-first | ✅ Yes | ⚠️ Added later | ❌ No | | Bundle size (minzipped) | ~14 KB | ~12 KB | ~25 KB | | Runtime type inference | ✅ z.infer<> | ❌ Manual types | ❌ Manual types | | Async validation | ✅ | ✅ | ✅ | | Error message customization | Good | Very good | Very good | | Best fit | TypeScript apps, LLM output | React forms, UX-focused | Node.js APIs, complex business rules |


Zod

Zod is the new default for TypeScript-first projects. The key insight that made it take off: the schema IS the type. You write the schema once and get both runtime validation and compile-time TypeScript types for free.

import { z } from 'zod';

const UserSchema = z.object({
  id:        z.number().int().positive(),
  name:      z.string().min(1).max(100),
  email:     z.string().email(),
  role:      z.enum(['admin', 'editor', 'viewer']),
  createdAt: z.string().datetime(),
  metadata:  z.record(z.string()).optional(),
});

// Free TypeScript type — no duplication
type User = z.infer<typeof UserSchema>;

// Validate at runtime
const user = UserSchema.parse(rawData);   // throws on invalid
const result = UserSchema.safeParse(rawData); // returns { success, data, error }

Why Zod wins for TypeScript projects

  1. Single source of truth. Your Zod schema replaces both the TypeScript interface and the runtime validator. Change the schema, type updates automatically.

  2. No type drift. With Yup or Joi, you write the schema and separately write the TypeScript interface. They drift apart silently over time.

  3. Composable and tree-shakeable. Schemas are values — you can pass them around, combine them, extend them.

  4. LLM output validation. When you're parsing JSON from an LLM, Zod's .safeParse() is perfect: it tells you exactly what was wrong without throwing.

const result = UserSchema.safeParse(llmOutput);
if (!result.success) {
  console.log(result.error.flatten());
  // { fieldErrors: { email: ['Invalid email'] }, formErrors: [] }
}

Generate Zod schemas automatically

You don't need to write Zod schemas by hand from existing JSON. Use the JSON to Zod Schema tool — paste a sample JSON object, get a complete Zod schema with named schemas, type exports, and format detection (email, UUID, datetime) automatically.

When Zod is NOT the right choice

  • Legacy JavaScript projects with no TypeScript — you lose most of the value
  • Complex async validation with many custom async checks — Zod's async support works but Joi's is more ergonomic for intricate cross-field dependencies
  • Huge validation performance requirements — Zod is fast but not the fastest option (see typia for extreme performance)

Yup

Yup predates Zod by 6 years and was the dominant validation library in React form tooling for most of that time. Formik made it famous; React Hook Form supports it as a resolver. It has excellent UX-focused features: field-level error messages, abortEarly: false to collect all errors at once, and clean async validation.

import * as yup from 'yup';

const userSchema = yup.object({
  id:    yup.number().integer().positive().required(),
  name:  yup.string().min(1).max(100).required(),
  email: yup.string().email().required(),
  role:  yup.mixed<'admin' | 'editor' | 'viewer'>()
             .oneOf(['admin', 'editor', 'viewer'])
             .required(),
});

// TypeScript type — requires manual annotation
type User = yup.InferType<typeof userSchema>;

// Validate — collects all errors, not just first
try {
  const user = await userSchema.validate(rawData, { abortEarly: false });
} catch (e) {
  if (e instanceof yup.ValidationError) {
    console.log(e.errors); // ['email is required', 'name is too short']
  }
}

Where Yup shines

  1. React form validation. Field-level error messages designed for UX: "Email is invalid" maps directly to an email input. React Hook Form's Yup resolver is battle-tested.

  2. User-facing error messages. Yup's .label() and message customization is more ergonomic for "Email is a required field" messages compared to Zod's .message().

  3. Conditional validation. Yup's .when() and .test() are mature and handle complex cross-field dependencies well.

const schema = yup.object({
  password: yup.string().required(),
  confirmPassword: yup.string()
    .oneOf([yup.ref('password')], 'Passwords must match')
    .required(),
});

Yup's weaknesses

TypeScript support is adequate but not TypeScript-first. yup.InferType<> works but is less precise than Zod's inference — optional fields and nullable handling requires more manual annotation. For server-side validation or LLM output validation, Zod is a better fit.


Joi

Joi is the oldest of the three and still widely used in Node.js API projects, particularly in the Express/hapi ecosystem where it originated. It has the most extensive built-in validation vocabulary — IP addresses, CIDR blocks, hostname validation, GUID formats — and the most powerful custom validation DSL for complex business rules.

import Joi from 'joi';

const userSchema = Joi.object({
  id:    Joi.number().integer().positive().required(),
  name:  Joi.string().min(1).max(100).required(),
  email: Joi.string().email().required(),
  role:  Joi.string().valid('admin', 'editor', 'viewer').required(),
  ip:    Joi.string().ip({ version: ['ipv4', 'ipv6'] }).optional(),
  tags:  Joi.array().items(Joi.string()).min(1).max(10),
});

const { error, value } = userSchema.validate(rawData, { abortEarly: false });
if (error) {
  console.log(error.details.map(d => d.message));
}

Where Joi shines

  1. Network/infrastructure validation. Built-in validators for IPs, CIDRs, hostnames, URIs, MACs — things you'd write custom code for in Zod.

  2. Complex business rules. Joi's Joi.alternatives(), Joi.when(), and the .custom() chain are the most expressive of the three for intricate multi-field dependencies.

  3. Runtime schema building. Joi schemas can be constructed programmatically from configuration objects — useful when validation rules come from a database or config file.

  4. hapi.js ecosystem. Joi is the native validation library for hapi.js and integrates deeply with its request lifecycle.

Joi's weaknesses

No native TypeScript types from schemas — you write the TypeScript interface separately. In 2026, this is a significant friction point. The bundle is also larger than Zod or Yup. For most TypeScript projects, the lack of automatic type inference is a dealbreaker.


Which One Should You Use?

Use Zod if:

  • Your project is TypeScript-first
  • You're validating API responses, LLM output, or any external JSON
  • You want your types and validators to always be in sync
  • You're using tRPC, Next.js, or any modern TypeScript stack

Use Yup if:

  • You're building React forms with React Hook Form or Formik
  • User-facing validation error messages are a priority
  • You're already on Yup and the migration cost outweighs the benefits

Use Joi if:

  • You're on a Node.js/Express or hapi.js backend
  • You need network-specific validators (IP, CIDR, hostname)
  • Your validation rules are complex enough to need Joi's DSL
  • You're maintaining a large existing Joi codebase

Migrating from Yup or Joi to Zod

If you're on Yup or Joi and want to migrate to Zod, the pattern is:

  1. Start with new code only — don't migrate existing schemas yet
  2. Use coexistence — Zod and Yup/Joi can run in the same project
  3. Migrate on change — when you touch a schema for another reason, convert it then

The main thing to watch for when migrating from Yup to Zod:

  • Yup is permissive by default (extra keys pass through); Zod strips extra keys with z.object().strict() or passes them through by default with z.object()
  • Yup's .nullable() and .optional() semantics differ slightly from Zod's

When migrating from Joi to Zod:

  • Map Joi.alternatives() to z.union()
  • Map Joi.when() to z.discriminatedUnion() or .superRefine()
  • Network validators need custom .refine() implementations

Quick Decision Tree

Do you have TypeScript?
├── No → Joi (most validation vocabulary) or Yup (React forms)
└── Yes
    ├── Building forms with React Hook Form / Formik?
    │   └── Yup (best form validation UX)
    └── API / LLM / general validation?
        └── Zod (TypeScript-first, automatic type inference)

Tools mentioned in this post:

Try the JSON Kit tools

Everything mentioned in this post is available as a free browser-side tool.

Browse all 30+ tools →