Yup

Validation LibraryJavaScriptReactFormsClient-sideFormik

Library

Yup

Overview

Yup is a lightweight schema validation library inspired by Joi, designed specifically for client-side validation. Built with excellent compatibility with form libraries like Formik and React Hook Form, it has gained popularity for form validation in React and other frontend frameworks. With its lightweight and simple API, Yup significantly contributes to productivity improvements in client-side development, making it the go-to choice for modern web application form handling.

Details

Yup 1.4.0 is the latest version as of 2025, delivering a frontend-optimized validation library that balances complete TypeScript support with lightweight design. While inheriting Joi's powerful schema definition concepts, it's optimized for browser environments. Seamless integration with major form libraries like React Hook Form, Formik, and React Final Form streamlines form development in modern web applications. Despite being lightweight, it provides powerful validation features that enhance user experience.

Key Features

  • Lightweight Design: Optimized library size for frontend use
  • Form Library Integration: Excellent compatibility with Formik, React Hook Form, etc.
  • TypeScript Support: Complete TypeScript support and type inference
  • Chaining API: Intuitive and readable schema definition approach
  • Async Validation: Promise-based asynchronous validation capabilities
  • High Customizability: Rich custom validation options

Pros and Cons

Pros

  • Outstanding usability in frontend frameworks
  • Powerful form development experience when combined with Formik
  • Lightweight with minimal impact on browser performance
  • Simple and easy-to-learn API design
  • Usable with frameworks beyond React
  • Active community and maintenance

Cons

  • Not suitable for server-side validation
  • Limited validation features compared to Joi
  • Constraints with complex validation logic
  • Unsupported usage in Node.js environments
  • Insufficient features for enterprise-level requirements
  • Documentation not as comprehensive as Joi's

Reference Pages

Code Examples

Installation and Basic Setup

# Install Yup
npm install yup

# TypeScript integration (type definitions built-in)
npm install @types/yup  # Usually not required

# Using Yarn
yarn add yup

Basic Schema Definition and Validation

import * as yup from 'yup';

// Basic schema definition
const userSchema = yup.object({
  name: yup.string()
    .min(2, 'Name must be at least 2 characters')
    .max(50, 'Name must be at most 50 characters')
    .required('Name is required'),
  
  email: yup.string()
    .email('Please enter a valid email address')
    .required('Email address is required'),
  
  age: yup.number()
    .integer('Age must be an integer')
    .min(18, 'Must be at least 18 years old')
    .max(120, 'Age must be at most 120 years')
    .required('Age is required'),
  
  website: yup.string()
    .url('Please enter a valid URL')
    .nullable(),
  
  agreeToTerms: yup.boolean()
    .oneOf([true], 'You must agree to the terms and conditions')
    .required()
});

// Data validation
const userData = {
  name: 'John Doe',
  email: '[email protected]',
  age: 30,
  website: 'https://example.com',
  agreeToTerms: true
};

// Synchronous validation
try {
  const validData = userSchema.validateSync(userData);
  console.log('Validation successful:', validData);
} catch (error) {
  console.log('Validation error:', error.errors);
}

// Asynchronous validation
async function validateUser(data) {
  try {
    const validData = await userSchema.validate(data);
    console.log('Validation successful:', validData);
    return validData;
  } catch (error) {
    console.log('Validation error:', error.errors);
    throw error;
  }
}

// Primitive type examples
const stringSchema = yup.string().min(3).max(20);
const numberSchema = yup.number().positive();
const booleanSchema = yup.boolean();
const dateSchema = yup.date();
const arraySchema = yup.array().of(yup.string());

// Test each schema
console.log(stringSchema.isValidSync('hello')); // true
console.log(numberSchema.isValidSync(42)); // true
console.log(booleanSchema.isValidSync(true)); // true

Advanced Validation Rules and Custom Validators

// Complex object schema
const registrationSchema = yup.object({
  // Detailed username validation
  username: yup.string()
    .min(3, 'Username must be at least 3 characters')
    .max(20, 'Username must be at most 20 characters')
    .matches(/^[a-zA-Z0-9_]+$/, 'Username can only contain alphanumeric characters and underscores')
    .required('Username is required'),
  
  // Complex password validation
  password: yup.string()
    .min(8, 'Password must be at least 8 characters')
    .matches(
      /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/,
      'Password must contain uppercase, lowercase, and numeric characters'
    )
    .required('Password is required'),
  
  // Password confirmation
  confirmPassword: yup.string()
    .oneOf([yup.ref('password')], 'Passwords do not match')
    .required('Password confirmation is required'),
  
  // Array validation
  hobbies: yup.array()
    .of(yup.string().min(2, 'Each hobby must be at least 2 characters'))
    .min(1, 'Please enter at least one hobby')
    .max(5, 'You can select up to 5 hobbies'),
  
  // Nested object
  address: yup.object({
    street: yup.string().required('Street address is required'),
    city: yup.string().required('City is required'),
    postalCode: yup.string()
      .matches(/^\d{5}-\d{4}$/, 'Invalid postal code format (e.g., 12345-6789)')
      .required('Postal code is required'),
    country: yup.string().default('USA')
  }),
  
  // Conditional field
  hasJobExperience: yup.boolean().required(),
  
  jobExperience: yup.string().when('hasJobExperience', {
    is: true,
    then: (schema) => schema.min(10, 'Job experience must be at least 10 characters').required('Job experience is required'),
    otherwise: (schema) => schema.notRequired()
  }),
  
  // Custom validation
  birthDate: yup.date()
    .max(new Date(), 'Birth date must be today or earlier')
    .test('age', 'Must be at least 18 years old', function(value) {
      const cutoff = new Date();
      cutoff.setFullYear(cutoff.getFullYear() - 18);
      return !value || value <= cutoff;
    })
    .required('Birth date is required')
});

// Dynamic schema example
const createDynamicSchema = (userType) => {
  let schema = yup.object({
    name: yup.string().required('Name is required'),
    email: yup.string().email().required('Email address is required')
  });

  if (userType === 'business') {
    schema = schema.shape({
      companyName: yup.string().required('Company name is required'),
      taxId: yup.string()
        .matches(/^\d{10,13}$/, 'Tax ID must be 10-13 digits')
        .required('Tax ID is required')
    });
  }

  if (userType === 'individual') {
    schema = schema.shape({
      phoneNumber: yup.string()
        .matches(/^[0-9-+().\s]+$/, 'Please enter a valid phone number')
        .required('Phone number is required')
    });
  }

  return schema;
};

// Custom validation function
const phoneNumberValidation = yup.string()
  .test('phone', 'Please enter a valid US phone number', function(value) {
    if (!value) return true; // Allow empty (handle separately with required)
    
    // US phone number patterns
    const patterns = [
      /^\+1-\d{3}-\d{3}-\d{4}$/, // +1-123-456-7890
      /^\(\d{3}\) \d{3}-\d{4}$/,  // (123) 456-7890
      /^\d{3}-\d{3}-\d{4}$/       // 123-456-7890
    ];
    
    return patterns.some(pattern => pattern.test(value));
  });

// Async custom validation
const uniqueEmailValidation = yup.string()
  .email()
  .test('unique-email', 'This email address is already in use', async function(value) {
    if (!value) return true;
    
    // API call for duplicate check (mock)
    try {
      const response = await fetch(`/api/check-email?email=${encodeURIComponent(value)}`);
      const data = await response.json();
      return data.available;
    } catch (error) {
      console.error('Email duplicate check error:', error);
      return true; // Allow on error
    }
  });

Framework Integration (React Hook Form, Formik, etc.)

// React Hook Form integration
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup';

const loginSchema = yup.object({
  email: yup.string()
    .email('Please enter a valid email address')
    .required('Email address is required'),
  password: yup.string()
    .min(6, 'Password must be at least 6 characters')
    .required('Password is required'),
  rememberMe: yup.boolean()
});

function LoginForm() {
  const { register, handleSubmit, formState: { errors } } = useForm({
    resolver: yupResolver(loginSchema)
  });

  const onSubmit = (data) => {
    console.log('Login data:', data);
    // Login processing
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        <input
          {...register('email')}
          type="email"
          placeholder="Email address"
        />
        {errors.email && <span className="error">{errors.email.message}</span>}
      </div>
      
      <div>
        <input
          {...register('password')}
          type="password"
          placeholder="Password"
        />
        {errors.password && <span className="error">{errors.password.message}</span>}
      </div>
      
      <div>
        <label>
          <input {...register('rememberMe')} type="checkbox" />
          Remember me
        </label>
      </div>
      
      <button type="submit">Login</button>
    </form>
  );
}

// Formik integration example
import { Formik, Form, Field, ErrorMessage } from 'formik';

const contactSchema = yup.object({
  name: yup.string()
    .min(2, 'Name must be at least 2 characters')
    .required('Name is required'),
  email: yup.string()
    .email('Please enter a valid email address')
    .required('Email address is required'),
  subject: yup.string()
    .min(5, 'Subject must be at least 5 characters')
    .required('Subject is required'),
  message: yup.string()
    .min(20, 'Message must be at least 20 characters')
    .max(500, 'Message must be at most 500 characters')
    .required('Message is required')
});

function ContactForm() {
  return (
    <Formik
      initialValues={{
        name: '',
        email: '',
        subject: '',
        message: ''
      }}
      validationSchema={contactSchema}
      onSubmit={(values, { setSubmitting }) => {
        console.log('Contact data:', values);
        // Send processing
        setSubmitting(false);
      }}
    >
      {({ isSubmitting }) => (
        <Form>
          <div>
            <Field type="text" name="name" placeholder="Your name" />
            <ErrorMessage name="name" component="div" className="error" />
          </div>
          
          <div>
            <Field type="email" name="email" placeholder="Email address" />
            <ErrorMessage name="email" component="div" className="error" />
          </div>
          
          <div>
            <Field type="text" name="subject" placeholder="Subject" />
            <ErrorMessage name="subject" component="div" className="error" />
          </div>
          
          <div>
            <Field as="textarea" name="message" placeholder="Message" rows={5} />
            <ErrorMessage name="message" component="div" className="error" />
          </div>
          
          <button type="submit" disabled={isSubmitting}>
            Send
          </button>
        </Form>
      )}
    </Formik>
  );
}

// React Final Form integration example
import { Form, Field } from 'react-final-form';

const profileSchema = yup.object({
  firstName: yup.string().required('First name is required'),
  lastName: yup.string().required('Last name is required'),
  bio: yup.string().max(200, 'Bio must be at most 200 characters')
});

function ProfileForm() {
  const validate = async (values) => {
    try {
      await profileSchema.validate(values, { abortEarly: false });
    } catch (error) {
      const errors = {};
      error.inner.forEach(err => {
        errors[err.path] = err.message;
      });
      return errors;
    }
  };

  return (
    <Form
      onSubmit={(values) => console.log('Profile data:', values)}
      validate={validate}
      render={({ handleSubmit, form, submitting, pristine, values }) => (
        <form onSubmit={handleSubmit}>
          <Field name="firstName">
            {({ input, meta }) => (
              <div>
                <input {...input} type="text" placeholder="First name" />
                {meta.error && meta.touched && <span className="error">{meta.error}</span>}
              </div>
            )}
          </Field>
          
          <Field name="lastName">
            {({ input, meta }) => (
              <div>
                <input {...input} type="text" placeholder="Last name" />
                {meta.error && meta.touched && <span className="error">{meta.error}</span>}
              </div>
            )}
          </Field>
          
          <Field name="bio">
            {({ input, meta }) => (
              <div>
                <textarea {...input} placeholder="Bio" />
                {meta.error && meta.touched && <span className="error">{meta.error}</span>}
              </div>
            )}
          </Field>
          
          <div>
            <button type="submit" disabled={submitting}>
              Save
            </button>
            <button
              type="button"
              onClick={form.reset}
              disabled={submitting || pristine}
            >
              Reset
            </button>
          </div>
        </form>
      )}
    />
  );
}

Error Handling and Custom Error Messages

// Detailed error handling
const productSchema = yup.object({
  name: yup.string()
    .min(2, 'Product name must be at least 2 characters')
    .max(100, 'Product name must be at most 100 characters')
    .required('Product name is required'),
  
  price: yup.number()
    .positive('Price must be a positive number')
    .min(1, 'Price must be at least $1')
    .max(1000000, 'Price must be at most $1,000,000')
    .required('Price is required'),
  
  category: yup.string()
    .oneOf(['electronics', 'clothing', 'books', 'food'], 'Please select a valid category')
    .required('Category is required'),
  
  description: yup.string()
    .min(10, 'Product description must be at least 10 characters')
    .max(1000, 'Product description must be at most 1000 characters')
    .required('Product description is required')
});

// Custom error handling function
function handleValidationErrors(error) {
  const fieldErrors = {};
  const generalErrors = [];
  
  if (error.inner && error.inner.length > 0) {
    // Multiple validation errors
    error.inner.forEach(err => {
      if (err.path) {
        fieldErrors[err.path] = err.message;
      } else {
        generalErrors.push(err.message);
      }
    });
  } else {
    // Single validation error
    if (error.path) {
      fieldErrors[error.path] = error.message;
    } else {
      generalErrors.push(error.message);
    }
  }
  
  return {
    hasErrors: true,
    fieldErrors,
    generalErrors,
    totalErrors: Object.keys(fieldErrors).length + generalErrors.length
  };
}

// Real-time validation implementation
class FormValidator {
  constructor(schema) {
    this.schema = schema;
    this.errors = {};
  }
  
  async validateField(fieldName, value, allValues = {}) {
    try {
      const fieldSchema = yup.reach(this.schema, fieldName);
      await fieldSchema.validate(value);
      
      // Field-level validation success
      delete this.errors[fieldName];
      
      // Overall schema validation (dependency check)
      try {
        await this.schema.validateAt(fieldName, { ...allValues, [fieldName]: value });
      } catch (contextError) {
        this.errors[fieldName] = contextError.message;
      }
      
    } catch (error) {
      this.errors[fieldName] = error.message;
    }
    
    return this.errors[fieldName] || null;
  }
  
  async validateForm(values) {
    try {
      await this.schema.validate(values, { abortEarly: false });
      this.errors = {};
      return { isValid: true, errors: {} };
    } catch (error) {
      const errorResult = handleValidationErrors(error);
      this.errors = errorResult.fieldErrors;
      return { isValid: false, errors: this.errors };
    }
  }
  
  getFieldError(fieldName) {
    return this.errors[fieldName] || null;
  }
  
  hasErrors() {
    return Object.keys(this.errors).length > 0;
  }
  
  clearErrors() {
    this.errors = {};
  }
}

// FormValidator usage example
const validator = new FormValidator(productSchema);

// Single field validation
async function validateProductName(name, allFormData) {
  const error = await validator.validateField('name', name, allFormData);
  console.log('Product name error:', error);
}

// Full form validation
async function validateProductForm(formData) {
  const result = await validator.validateForm(formData);
  if (!result.isValid) {
    console.log('Validation errors:', result.errors);
  }
  return result;
}

// Internationalized error messages
const createLocalizedSchema = (locale = 'en') => {
  const messages = {
    en: {
      required: 'This field is required',
      email: 'Please enter a valid email address',
      min: 'Must be at least ${min} characters',
      max: 'Must be at most ${max} characters'
    },
    ja: {
      required: 'このフィールドは必須項目です',
      email: '有効なメールアドレスを入力してください',
      min: '${min}文字以上で入力してください',
      max: '${max}文字以下で入力してください'
    }
  };
  
  const currentMessages = messages[locale] || messages.en;
  
  return yup.object({
    email: yup.string()
      .email(currentMessages.email)
      .required(currentMessages.required),
    
    password: yup.string()
      .min(8, currentMessages.min)
      .max(50, currentMessages.max)
      .required(currentMessages.required)
  });
};

// Usage example
const enSchema = createLocalizedSchema('en');
const jaSchema = createLocalizedSchema('ja');

Type Safety and TypeScript Integration

// Type-safe Yup schemas for TypeScript
import * as yup from 'yup';
import { InferType } from 'yup';

// Schema definition
const userProfileSchema = yup.object({
  id: yup.number().required(),
  username: yup.string().min(3).max(20).required(),
  email: yup.string().email().required(),
  age: yup.number().integer().min(18).max(120).required(),
  isActive: yup.boolean().default(true),
  preferences: yup.object({
    theme: yup.string().oneOf(['light', 'dark']).default('light'),
    notifications: yup.boolean().default(true),
    language: yup.string().oneOf(['en', 'ja']).default('en')
  }),
  tags: yup.array().of(yup.string()).default([]),
  createdAt: yup.date().default(() => new Date()),
  lastLogin: yup.date().nullable().default(null)
});

// Automatic type inference
type UserProfile = InferType<typeof userProfileSchema>;
/*
type UserProfile = {
  id: number;
  username: string;
  email: string;
  age: number;
  isActive: boolean;
  preferences: {
    theme: "light" | "dark";
    notifications: boolean;
    language: "en" | "ja";
  };
  tags: string[];
  createdAt: Date;
  lastLogin: Date | null;
}
*/

// Type-safe validation function
async function validateUserProfile(data: unknown): Promise<UserProfile> {
  try {
    const validatedData = await userProfileSchema.validate(data, {
      stripUnknown: true,
      abortEarly: false
    });
    return validatedData; // TypeScript infers the type
  } catch (error) {
    if (error instanceof yup.ValidationError) {
      throw new Error(`Validation error: ${error.errors.join(', ')}`);
    }
    throw error;
  }
}

// Generic type-safe validator class
class TypeSafeValidator<T extends yup.AnyObject> {
  constructor(private schema: yup.ObjectSchema<T>) {}

  async validate(data: unknown): Promise<InferType<yup.ObjectSchema<T>>> {
    return await this.schema.validate(data, { stripUnknown: true });
  }

  async validatePartial(data: unknown): Promise<Partial<InferType<yup.ObjectSchema<T>>>> {
    // Partial validation (temporarily ignore required fields)
    const partialSchema = this.schema.partial();
    return await partialSchema.validate(data, { stripUnknown: true });
  }

  isValid(data: unknown): boolean {
    return this.schema.isValidSync(data);
  }

  getErrors(data: unknown): string[] {
    try {
      this.schema.validateSync(data, { abortEarly: false });
      return [];
    } catch (error) {
      if (error instanceof yup.ValidationError) {
        return error.errors;
      }
      return ['An unexpected error occurred'];
    }
  }
}

// Type-safe validator usage
const userValidator = new TypeSafeValidator(userProfileSchema);

// React Hook for type-safe usage
import { useState, useCallback } from 'react';

interface FormState<T> {
  data: Partial<T>;
  errors: Record<string, string>;
  isValid: boolean;
  isDirty: boolean;
}

function useYupForm<T extends yup.AnyObject>(
  schema: yup.ObjectSchema<T>,
  initialData: Partial<InferType<yup.ObjectSchema<T>>> = {}
) {
  const [formState, setFormState] = useState<FormState<InferType<yup.ObjectSchema<T>>>>({
    data: initialData,
    errors: {},
    isValid: false,
    isDirty: false
  });

  const validateField = useCallback(async (fieldName: string, value: any) => {
    try {
      await yup.reach(schema, fieldName).validate(value);
      setFormState(prev => ({
        ...prev,
        errors: { ...prev.errors, [fieldName]: '' }
      }));
    } catch (error) {
      if (error instanceof yup.ValidationError) {
        setFormState(prev => ({
          ...prev,
          errors: { ...prev.errors, [fieldName]: error.message }
        }));
      }
    }
  }, [schema]);

  const setValue = useCallback((fieldName: string, value: any) => {
    setFormState(prev => ({
      ...prev,
      data: { ...prev.data, [fieldName]: value },
      isDirty: true
    }));
    validateField(fieldName, value);
  }, [validateField]);

  const validateForm = useCallback(async (): Promise<boolean> => {
    try {
      await schema.validate(formState.data, { abortEarly: false });
      setFormState(prev => ({ ...prev, errors: {}, isValid: true }));
      return true;
    } catch (error) {
      if (error instanceof yup.ValidationError) {
        const errors: Record<string, string> = {};
        error.inner.forEach(err => {
          if (err.path) {
            errors[err.path] = err.message;
          }
        });
        setFormState(prev => ({ ...prev, errors, isValid: false }));
      }
      return false;
    }
  }, [schema, formState.data]);

  const reset = useCallback(() => {
    setFormState({
      data: initialData,
      errors: {},
      isValid: false,
      isDirty: false
    });
  }, [initialData]);

  return {
    ...formState,
    setValue,
    validateForm,
    reset,
    setFieldError: (fieldName: string, error: string) => {
      setFormState(prev => ({
        ...prev,
        errors: { ...prev.errors, [fieldName]: error }
      }));
    }
  };
}

// Custom type-safe schema builder
const createTypedSchema = <T>() => ({
  string: () => yup.string() as yup.StringSchema<string>,
  number: () => yup.number() as yup.NumberSchema<number>,
  boolean: () => yup.boolean() as yup.BooleanSchema<boolean>,
  date: () => yup.date() as yup.DateSchema<Date>,
  array: <U>(itemSchema: yup.Schema<U>) => yup.array().of(itemSchema) as yup.ArraySchema<U[]>,
  object: <U extends Record<string, any>>(shape: yup.ObjectShape) => 
    yup.object(shape) as yup.ObjectSchema<U>
});

// Usage example: Fully type-safe schema creation
const typedBuilder = createTypedSchema<UserProfile>();

const strictUserSchema = typedBuilder.object<UserProfile>({
  id: typedBuilder.number().required(),
  username: typedBuilder.string().min(3).required(),
  email: typedBuilder.string().email().required(),
  age: typedBuilder.number().integer().min(18).required(),
  isActive: typedBuilder.boolean().default(true),
  // ... other fields
});