# Validation Libraries: Joi vs Yup vs Zod

# 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

| Aspect | Joi | Yup | Zod |
|--------|-----|-----|-----|
| **Bundle Size** | ~90KB | ~15KB | ~8KB |
| **TypeScript** | Partial | Good | Excellent |
| **Type Inference** | Manual | Manual | Automatic |
| **Learning Curve** | Steep | Moderate | Gentle |
| **Async Support** | Yes | Yes | Yes |
| **Best For** | Enterprise | React Forms | Modern TS Apps |

---

## Code Examples

### 1. Joi - Enterprise-Grade Validation

**Use Case:** Large applications with complex validation rules

```javascript
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

```javascript
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

```typescript
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

```markdown
| 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.
