Yup Complete Guide | Schema Validation for JavaScript

Yup Complete Guide | Schema Validation for JavaScript

이 글의 핵심

Yup is a schema-based validation library for JavaScript. It provides expressive schema definitions, async validation, and excellent integration with form libraries.

Introduction

Yup is a JavaScript schema builder for value parsing and validation. It’s widely used with form libraries like Formik and React Hook Form.

Without Yup

function validateUser(data) {
  const errors = {};
  
  if (!data.name) {
    errors.name = 'Name is required';
  } else if (data.name.length < 2) {
    errors.name = 'Name must be at least 2 characters';
  }
  
  if (!data.email) {
    errors.email = 'Email is required';
  } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) {
    errors.email = 'Invalid email format';
  }
  
  if (!data.age) {
    errors.age = 'Age is required';
  } else if (data.age < 18) {
    errors.age = 'Must be 18 or older';
  }
  
  return Object.keys(errors).length > 0 ? errors : null;
}

With Yup

import * as yup from 'yup';

const userSchema = yup.object({
  name: yup.string().required().min(2),
  email: yup.string().required().email(),
  age: yup.number().required().min(18),
});

await userSchema.validate(data);

1. Installation

npm install yup

2. Basic Schemas

import * as yup from 'yup';

// String
const nameSchema = yup.string();
const emailSchema = yup.string().email();
const urlSchema = yup.string().url();

// Number
const ageSchema = yup.number();
const priceSchema = yup.number().positive();
const quantitySchema = yup.number().integer().min(1);

// Boolean
const acceptedSchema = yup.boolean().oneOf([true]);

// Date
const birthdaySchema = yup.date();
const futureSchema = yup.date().min(new Date());

// Array
const tagsSchema = yup.array().of(yup.string());
const numbersSchema = yup.array().of(yup.number()).min(1).max(5);

// Object
const addressSchema = yup.object({
  street: yup.string().required(),
  city: yup.string().required(),
  zipCode: yup.string().matches(/^\d{5}$/),
});

3. Validation Methods

const schema = yup.object({
  name: yup.string().required(),
  email: yup.string().email().required(),
});

// Validate and throw on error
try {
  await schema.validate({ name: 'Alice', email: 'invalid' });
} catch (error) {
  console.error(error.message);
}

// Validate and return value or undefined
const result = await schema.isValid({ name: 'Alice', email: 'alice@example.com' });
console.log(result); // true

// Validate and get all errors
try {
  await schema.validate({ name: '', email: 'invalid' }, { abortEarly: false });
} catch (error) {
  console.log(error.errors); // Array of all error messages
}

// Validate synchronously (no async rules)
try {
  schema.validateSync({ name: 'Alice', email: 'alice@example.com' });
} catch (error) {
  console.error(error);
}

4. Required and Optional

import * as yup from 'yup';

const schema = yup.object({
  // Required
  name: yup.string().required('Name is required'),
  
  // Optional (nullable)
  middleName: yup.string().nullable(),
  
  // Optional (default value)
  role: yup.string().default('user'),
  
  // Optional (undefined allowed)
  bio: yup.string().notRequired(),
  
  // Conditionally required
  phoneNumber: yup.string().when('contactMethod', {
    is: 'phone',
    then: (schema) => schema.required(),
    otherwise: (schema) => schema.notRequired(),
  }),
});

5. String Validation

yup.string()
  .required('Required')
  .min(2, 'Must be at least 2 characters')
  .max(50, 'Must be less than 50 characters')
  .email('Invalid email')
  .url('Invalid URL')
  .matches(/^[a-zA-Z]+$/, 'Only letters allowed')
  .trim() // Remove whitespace
  .lowercase() // Convert to lowercase
  .uppercase(); // Convert to uppercase

// Custom validation
yup.string().test('is-strong-password', 'Password too weak', (value) => {
  return /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/.test(value);
});

6. Number Validation

yup.number()
  .required()
  .min(0, 'Must be non-negative')
  .max(100, 'Must be 100 or less')
  .positive('Must be positive')
  .negative('Must be negative')
  .integer('Must be an integer')
  .lessThan(10)
  .moreThan(0)
  .round('floor'); // 'floor', 'ceil', 'trunc', 'round'

7. Object Validation

const userSchema = yup.object({
  name: yup.string().required(),
  email: yup.string().email().required(),
  address: yup.object({
    street: yup.string().required(),
    city: yup.string().required(),
    country: yup.string().required(),
  }),
  settings: yup.object().shape({
    notifications: yup.boolean().default(true),
    theme: yup.string().oneOf(['light', 'dark']).default('light'),
  }),
});

// Validate
await userSchema.validate({
  name: 'Alice',
  email: 'alice@example.com',
  address: {
    street: '123 Main St',
    city: 'New York',
    country: 'USA',
  },
});

8. Array Validation

// Array of strings
const tagsSchema = yup.array()
  .of(yup.string())
  .min(1, 'At least one tag required')
  .max(5, 'Maximum 5 tags allowed');

// Array of objects
const usersSchema = yup.array().of(
  yup.object({
    id: yup.number().required(),
    name: yup.string().required(),
    email: yup.string().email().required(),
  })
);

// Unique items
const uniqueEmailsSchema = yup.array()
  .of(yup.string().email())
  .test('unique', 'Emails must be unique', (values) => {
    return values.length === new Set(values).size;
  });

await tagsSchema.validate(['react', 'typescript', 'nextjs']);

9. Conditional Validation

const schema = yup.object({
  accountType: yup.string().oneOf(['personal', 'business']).required(),
  
  // Required only for business accounts
  companyName: yup.string().when('accountType', {
    is: 'business',
    then: (schema) => schema.required('Company name is required'),
    otherwise: (schema) => schema.notRequired(),
  }),
  
  // Multiple conditions
  taxId: yup.string().when(['accountType', 'country'], {
    is: (accountType, country) => accountType === 'business' && country === 'US',
    then: (schema) => schema.required('Tax ID required for US businesses'),
  }),
});

10. Custom Validation

// Custom test
const passwordSchema = yup.string()
  .test('strong-password', 'Password must include uppercase, lowercase, and number', 
    (value) => {
      if (!value) return false;
      return /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/.test(value);
    }
  );

// Async validation
const emailSchema = yup.string()
  .email()
  .test('unique-email', 'Email already exists', async (value) => {
    const exists = await checkEmailExists(value);
    return !exists;
  });

// Access other fields
const schema = yup.object({
  password: yup.string().required().min(8),
  confirmPassword: yup.string()
    .required()
    .oneOf([yup.ref('password')], 'Passwords must match'),
});

11. TypeScript Integration

import * as yup from 'yup';
import type { InferType } from 'yup';

const userSchema = yup.object({
  name: yup.string().required(),
  email: yup.string().email().required(),
  age: yup.number().required().positive().integer(),
  website: yup.string().url().nullable(),
});

// Infer TypeScript type from schema
type User = InferType<typeof userSchema>;

// Type is:
// {
//   name: string;
//   email: string;
//   age: number;
//   website: string | null;
// }

async function createUser(data: unknown) {
  const user: User = await userSchema.validate(data);
  return user; // Fully typed!
}

12. React Hook Form Integration

npm install react-hook-form @hookform/resolvers
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup';

const schema = yup.object({
  name: yup.string().required('Name is required').min(2),
  email: yup.string().required('Email is required').email('Invalid email'),
  age: yup.number().required('Age is required').positive().integer().min(18),
});

function SignupForm() {
  const { register, handleSubmit, formState: { errors } } = useForm({
    resolver: yupResolver(schema),
  });
  
  const onSubmit = (data) => {
    console.log(data); // Validated data
  };
  
  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register('name')} />
      {errors.name && <p>{errors.name.message}</p>}
      
      <input {...register('email')} type="email" />
      {errors.email && <p>{errors.email.message}</p>}
      
      <input {...register('age')} type="number" />
      {errors.age && <p>{errors.age.message}</p>}
      
      <button type="submit">Submit</button>
    </form>
  );
}

13. Custom Error Messages

const schema = yup.object({
  name: yup.string()
    .required('이름을 입력해주세요')
    .min(2, '이름은 최소 2자 이상이어야 합니다'),
  
  email: yup.string()
    .required('이메일을 입력해주세요')
    .email('올바른 이메일 형식이 아닙니다'),
  
  age: yup.number()
    .required('나이를 입력해주세요')
    .min(18, '18세 이상만 가입할 수 있습니다')
    .max(100, '나이는 100세 이하여야 합니다'),
});

// With interpolation
yup.string()
  .min(5, 'Must be at least ${min} characters')
  .max(20, 'Must be at most ${max} characters');

14. Best Practices

1. Reuse Schemas

// Shared schemas
const emailSchema = yup.string().email().required();
const passwordSchema = yup.string().required().min(8);

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

const signupSchema = yup.object({
  email: emailSchema,
  password: passwordSchema,
  confirmPassword: yup.string()
    .required()
    .oneOf([yup.ref('password')]),
});

2. Type-Safe Schemas

import * as yup from 'yup';

const createUserSchema = <T extends yup.AnyObject>(extraFields: T) => {
  return yup.object({
    name: yup.string().required(),
    email: yup.string().email().required(),
    ...extraFields,
  });
};

const adminSchema = createUserSchema({
  role: yup.string().oneOf(['admin', 'superadmin']).required(),
});

3. Separate Validation Logic

// validators/user.js
export const userValidators = {
  name: yup.string().required().min(2),
  email: yup.string().required().email(),
  age: yup.number().required().min(18),
};

// forms/signup.js
import { userValidators } from '../validators/user';

const signupSchema = yup.object({
  ...userValidators,
  password: yup.string().required().min(8),
});

Summary

Yup provides powerful schema-based validation:

  • Expressive API for defining schemas
  • TypeScript support with type inference
  • Async validation for server checks
  • Deep nesting for complex objects
  • Great ecosystem integration

Key Takeaways:

  1. Define schemas once, use everywhere
  2. Use InferType for TypeScript types
  3. Custom validation with test()
  4. Conditional validation with when()
  5. Integrates with React Hook Form

Next Steps:

Resources: