Skip to main content

Command Palette

Search for a command to run...

Validation Libraries: Joi vs Yup vs Zod

Learn: Validation Libraries: Joi vs Yup vs Zod

Updated
6 min readView as Markdown
T

Welcome to TopperBlog! 👋

I'm a tech content creator passionate about helping developers level up their careers and master cutting-edge technologies.

🎯 What I Write About: • AI/ML Engineering & LLMs • Web3 & Blockchain Development
• System Design & Architecture • Interview Preparation (FAANG) • Freelancing & Remote Work • Modern Tech Stacks (Next.js, React, Rust, TypeScript) • Performance Optimization & Best Practices

💼 Mission: Sharing practical, actionable insights that accelerate your tech career and maximize your earning potential.

📚 15+ In-Depth Guides covering everything from earning $10k/month as a freelancer to cracking FAANG interviews.

🌐 Let's connect and grow together in this amazing tech journey!

#TechBlogger #SoftwareEngineering #CareerGrowth #WebDevelopment #AIEngineering

Validation Libraries: Joi vs Yup vs Zod

Problem

Modern applications require robust data validation across multiple layers—API inputs, form submissions, database operations, and configuration files. Choosing the right validation library impacts code maintainability, bundle size, type safety, and developer experience.

Key Challenges:

  • Type safety and TypeScript integration
  • Bundle size and performance
  • API ergonomics and learning curve
  • Error message customization
  • Async validation support
  • Schema reusability

Solution Overview

AspectJoiYupZod
Bundle Size~90KB~15KB~8KB
TypeScriptPartialGoodExcellent
Type InferenceManualManualAutomatic
Learning CurveSteepModerateGentle
Async SupportYesYesYes
Best ForEnterpriseReact FormsModern TS Apps

Code Examples

1. Joi - Enterprise-Grade Validation

Use Case: Large applications with complex validation rules

const Joi = require('joi');

// Define schema
const userSchema = Joi.object({
  email: Joi.string()
    .email({ tlds: { allow: false } })
    .required()
    .messages({
      'string.email': 'Please provide a valid email',
      'any.required': 'Email is required'
    }),

  password: Joi.string()
    .min(8)
    .pattern(/^(?=.*[A-Z])(?=.*\d)/)
    .required()
    .messages({
      'string.pattern.base': 'Password must contain uppercase and number'
    }),

  age: Joi.number()
    .integer()
    .min(18)
    .max(120)
    .optional(),

  role: Joi.string()
    .valid('admin', 'user', 'guest')
    .default('user'),

  profile: Joi.object({
    firstName: Joi.string().required(),
    lastName: Joi.string().required(),
    bio: Joi.string().max(500)
  }).optional()
});

// Validation
const data = {
  email: 'user@example.com',
  password: 'SecurePass123',
  role: 'admin',
  profile: {
    firstName: 'John',
    lastName: 'Doe'
  }
};

const { error, value } = userSchema.validate(data);

if (error) {
  console.error('Validation failed:', error.details);
} else {
  console.log('Valid data:', value);
}

// Async validation with external checks
const advancedSchema = Joi.object({
  username: Joi.string()
    .alphanum()
    .min(3)
    .max(30)
    .required()
    .external(async (value) => {
      const exists = await checkUsernameInDatabase(value);
      if (exists) {
        throw new Error('Username already taken');
      }
    })
});

Pros:

  • Comprehensive validation rules
  • Excellent for complex enterprise schemas
  • Strong error customization

Cons:

  • Largest bundle size
  • Verbose syntax
  • Weak TypeScript inference

2. Yup - React Forms Favorite

Use Case: React applications with Formik integration

import * as yup from 'yup';

// Define schema
const registrationSchema = yup.object().shape({
  email: yup
    .string()
    .email('Invalid email format')
    .required('Email is required'),

  password: yup
    .string()
    .min(8, 'Password must be at least 8 characters')
    .matches(
      /^(?=.*[A-Z])(?=.*\d)/,
      'Must contain uppercase letter and number'
    )
    .required('Password is required'),

  confirmPassword: yup
    .string()
    .oneOf([yup.ref('password')], 'Passwords must match')
    .required('Confirm password is required'),

  age: yup
    .number()
    .typeError('Age must be a number')
    .min(18, 'Must be 18 or older')
    .max(120, 'Invalid age'),

  terms: yup
    .boolean()
    .oneOf([true], 'You must accept terms')
    .required()
});

// Validation
async function validateUser(data) {
  try {
    const validData = await registrationSchema.validate(data, {
      abortEarly: false // Get all errors, not just first
    });
    console.log('Valid:', validData);
  } catch (error) {
    console.error('Errors:', error.inner);
    // error.inner = [{ path: 'email', message: '...' }, ...]
  }
}

// React + Formik Integration
import { Formik, Form, Field, ErrorMessage } from 'formik';

function RegistrationForm() {
  return (
    <Formik
      initialValues={{
        email: '',
        password: '',
        confirmPassword: '',
        age: '',
        terms: false
      }}
      validationSchema={registrationSchema}
      onSubmit={(values) => {
        console.log('Submitting:', values);
      }}
    >
      {({ errors, touched }) => (
        <Form>
          <Field name="email" type="email" />
          <ErrorMessage name="email" />

          <Field name="password" type="password" />
          <ErrorMessage name="password" />

          <button type="submit">Register</button>
        </Form>
      )}
    </Formik>
  );
}

// Conditional validation
const conditionalSchema = yup.object().shape({
  userType: yup.string().oneOf(['individual', 'business']),

  businessName: yup
    .string()
    .when('userType', {
      is: 'business',
      then: yup.string().required('Business name required'),
      otherwise: yup.string().notRequired()
    })
});

Pros:

  • Perfect Formik integration
  • Smaller than Joi
  • Good error handling

Cons:

  • Manual TypeScript types
  • No automatic type inference
  • Moderate bundle size

3. Zod - Modern TypeScript First

Use Case: TypeScript projects requiring type safety

import { z } from 'zod';

// Define schema with automatic type inference
const userSchema = z.object({
  email: z
    .string()
    .email('Invalid email')
    .describe('User email address'),

  password: z
    .string()
    .min(8, 'Minimum 8 characters')
    .regex(/^(?=.*[A-Z])(?=.*\d)/, 'Must contain uppercase and number'),

  age: z
    .number()
    .int()
    .min(18)
    .max(120)
    .optional(),

  role: z
    .enum(['admin', 'user', 'guest'])
    .default('user'),

  profile: z.object({
    firstName: z.string(),
    lastName: z.string(),
    bio: z.string().max(500).optional()
  }).optional()
});

// Automatic type inference
type User = z.infer<typeof userSchema>;
// type User = {
//   email: string;
//   password: string;
//   age?: number;
//   role: 'admin' | 'user' | 'guest';
//   profile?: { firstName: string; lastName: string; bio?: string };
// }

// Validation
const data = {
  email: 'user@example.com',
  password: 'SecurePass123',
  role: 'admin' as const
};

try {
  const validUser: User = userSchema.parse(data);
  console.log('Valid:', validUser);
} catch (error) {
  if (error instanceof z.ZodError) {
    console.error('Validation errors:', error.errors);
    // error.errors = [{ code, path, message }, ...]
  }
}

// Safe parsing (no throw)
const result = userSchema.safeParse(data);
if (result.success) {
  console.log('Valid:', result.data);
} else {
  console.error('Errors:', result.error.flatten());
}

// Async validation
const advancedSchema = z.object({
  username: z
    .string()
    .min(3)
    .refine(
      async (val) => !(await checkUsernameExists(val)),
      { message: 'Username already taken' }
    )
});

// Discriminated unions
const petSchema = z.discriminatedUnion('type', [
  z.object({ type: z.literal('dog'), breed: z.string() }),
  z.object({ type: z.literal('cat'), color: z.string() })
]);

// Transformations
const transformSchema = z.object({
  email: z.string().email().transform((val) => val.toLowerCase()),
  age: z.string().pipe(z.coerce.number().min(18))
});

// API validation
async function handleUserSubmission(req: Request) {
  try {
    const user = await userSchema.parseAsync(req.body);
    // user is fully typed as User
    return { success: true, data: user };
  } catch (error) {
    if (error instanceof z.ZodError) {
      return { success: false, errors: error.flatten() };
    }
  }
}

// Reusable schemas
const emailSchema = z.string().email();
const passwordSchema = z.string().min(8);

const loginSchema = z.object({
  email: emailSchema,
  password: passwordSchema
});

const signupSchema = loginSchema.extend({
  confirmPassword: passwordSchema
}).refine((data) => data.password === data.confirmPassword, {
  message: 'Passwords do not match',
  path: ['confirmPassword']
});

Pros:

  • Smallest bundle size
  • Automatic TypeScript inference
  • Modern, intuitive API
  • Excellent error handling
  • Built-in transformations

Cons:

  • Newer ecosystem
  • Fewer integrations
  • Smaller community

Comparison Table

| Feature | Joi | Yup | Zod |
|---------|-----|-----|-----|
| Bundle Size | 90KB | 15KB | 8KB |
| TypeScript Support | ⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| Type Inference | Manual | Manual | Automatic |
| Async Validation | ✅ | ✅ | ✅ |
| Error Messages | Excellent | Good | Excellent |
| Learning Curve | Steep | Moderate | Gentle |
| React Integration | Limited | Excellent | Good |
| API Validation | Excellent | Good | Excellent |
| Transformations | Limited | Good | Excellent |
| Community | Large | Large | Growing |

Decision Guide

Choose Joi if:

  • Building enterprise applications
  • Need maximum validation flexibility
  • Team familiar with Joi ecosystem

Choose Yup if:

  • Using React with Formik
  • Need form validation focus
  • Want smaller bundle than Joi

Choose Zod if:

  • Using TypeScript
  • Want smallest bundle size
  • Need automatic type inference
  • Building modern applications

Conclusion

All three libraries excel at schema validation. Zod leads for TypeScript projects with automatic inference and minimal bundle size. Yup remains ideal for React forms. Joi dominates enterprise scenarios requiring complex validation logic. Choose based on your project's TypeScript adoption, bundle size constraints, and integration needs.