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
-
Single source of truth. Your Zod schema replaces both the TypeScript interface and the runtime validator. Change the schema, type updates automatically.
-
No type drift. With Yup or Joi, you write the schema and separately write the TypeScript interface. They drift apart silently over time.
-
Composable and tree-shakeable. Schemas are values — you can pass them around, combine them, extend them.
-
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
-
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. -
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(). -
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
-
Network/infrastructure validation. Built-in validators for IPs, CIDRs, hostnames, URIs, MACs — things you'd write custom code for in Zod.
-
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. -
Runtime schema building. Joi schemas can be constructed programmatically from configuration objects — useful when validation rules come from a database or config file.
-
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:
- Start with new code only — don't migrate existing schemas yet
- Use coexistence — Zod and Yup/Joi can run in the same project
- 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 withz.object() - Yup's
.nullable()and.optional()semantics differ slightly from Zod's
When migrating from Joi to Zod:
- Map
Joi.alternatives()toz.union() - Map
Joi.when()toz.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:
- JSON to Zod Schema — generate named Zod schemas from any JSON sample
- JSON to TypeScript — generate TypeScript interfaces from JSON
- Fix LLM JSON — repair broken JSON before validating