Zod vs Yup in 2026: Schema Validation Libraries Compared
·PkgPulse Team
TL;DR
Zod for TypeScript projects, especially with tRPC or React Hook Form. Yup for async validation-heavy forms. Zod (~20M weekly downloads) has better TypeScript inference and has overtaken Yup as the community standard. Yup (~12M downloads) has better async validation patterns and a Formik-first API that some teams prefer. For any new TypeScript project, start with Zod.
Key Takeaways
- Zod: ~20M weekly downloads — Yup: ~12M (npm, March 2026)
- Zod infers TypeScript types automatically — Yup requires manual type annotation
- Yup has better async validation — test() methods are async by default
- Both integrate with React Hook Form — via resolvers
- Zod has better error handling — structured ZodError with path info
Schema Definition
// Yup — method chaining, JavaScript-first
import * as yup from 'yup';
const signupSchema = yup.object({
username: yup.string()
.required('Username is required')
.min(3, 'Must be at least 3 characters')
.max(20, 'Must be at most 20 characters')
.matches(/^[a-zA-Z0-9_]+$/, 'Only letters, numbers, and underscores'),
email: yup.string()
.required('Email is required')
.email('Must be a valid email'),
password: yup.string()
.required('Password is required')
.min(8, 'Must be at least 8 characters'),
confirmPassword: yup.string()
.oneOf([yup.ref('password')], 'Passwords must match'),
age: yup.number().min(13, 'Must be 13 or older').nullable(),
});
// TypeScript type — must be inferred separately
type SignupType = yup.InferType<typeof signupSchema>;
// Works, but less clean than Zod
// Zod — TypeScript-first schema definition
import { z } from 'zod';
const signupSchema = z.object({
username: z.string()
.min(3, 'Must be at least 3 characters')
.max(20, 'Must be at most 20 characters')
.regex(/^[a-zA-Z0-9_]+$/, 'Only letters, numbers, and underscores'),
email: z.string().email('Must be a valid email'),
password: z.string().min(8, 'Must be at least 8 characters'),
confirmPassword: z.string(),
age: z.number().min(13, 'Must be 13 or older').nullable().optional(),
}).refine(
(data) => data.password === data.confirmPassword,
{ message: 'Passwords must match', path: ['confirmPassword'] }
);
type Signup = z.infer<typeof signupSchema>;
// Signup = { username: string; email: string; password: string; ... }
// Automatically inferred — no separate type annotation needed
Async Validation
// Yup — async test() method is straightforward
const usernameSchema = yup.string()
.required()
.test('unique', 'Username is already taken', async (value) => {
if (!value) return true;
const exists = await checkUsernameAvailability(value);
return !exists;
});
// Yup validates async schemas natively:
try {
await usernameSchema.validate('alice');
} catch (err) {
// Validation error with message
}
// Zod — async validation via superRefine
const usernameSchema = z.string()
.min(3)
.superRefine(async (value, ctx) => {
const exists = await checkUsernameAvailability(value);
if (exists) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Username is already taken',
});
}
});
// Must use parseAsync() for async schemas
const result = await usernameSchema.safeParseAsync('alice');
// Note: Zod's async API is less ergonomic than Yup's
React Hook Form Integration
// Both work with React Hook Form via @hookform/resolvers
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { yupResolver } from '@hookform/resolvers/yup';
// Zod
const { register, handleSubmit, formState: { errors } } = useForm({
resolver: zodResolver(signupSchema),
});
// Yup
const { register, handleSubmit, formState: { errors } } = useForm({
resolver: yupResolver(signupSchema),
});
// Both produce the same ergonomics in the component — equal here
Formik + Yup (Legacy Stack)
// Yup was built alongside Formik — deep integration
import { Formik, Form, Field } from 'formik';
const LoginForm = () => (
<Formik
initialValues={{ email: '', password: '' }}
validationSchema={loginSchema} // Yup schema — native Formik support
onSubmit={(values) => login(values)}
>
{({ errors, touched }) => (
<Form>
<Field name="email" type="email" />
{errors.email && touched.email && <div>{errors.email}</div>}
<Field name="password" type="password" />
<button type="submit">Login</button>
</Form>
)}
</Formik>
);
If your stack includes Formik, Yup's native integration is still an advantage. But React Hook Form + Zod has become the more popular combination.
Error Messages
// Zod — structured errors with paths
const result = signupSchema.safeParse({ email: 'invalid', password: '123' });
if (!result.success) {
// Access specific field errors
const emailError = result.error.issues.find(i => i.path[0] === 'email');
const allErrors = result.error.flatten().fieldErrors;
// { email: ['Must be a valid email'], password: ['Must be at least 8 characters'] }
}
// Yup — ValidationError with path
try {
await signupSchema.validate(data, { abortEarly: false }); // Collect all errors
} catch (err) {
if (err instanceof yup.ValidationError) {
const errors = err.inner.reduce((acc, e) => ({
...acc,
[e.path]: e.message,
}), {});
// { email: 'Must be a valid email', password: 'Must be at least 8 characters' }
}
}
When to Choose
Choose Zod when:
- TypeScript project (automatic type inference is a major DX win)
- Using tRPC or libraries with native Zod support
- Using React Hook Form (both work, but Zod is more popular today)
- New project without existing validation library commitment
Choose Yup when:
- Using Formik (native integration, less setup)
- Async validation is complex and central to your forms
- Existing codebase with Yup already integrated
- JavaScript (not TypeScript) project where Zod's DX advantage is smaller
Compare Zod and Yup package health on PkgPulse.
See the live comparison
View zod vs. yup on PkgPulse →