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:
- Define schemas once, use everywhere
- Use InferType for TypeScript types
- Custom validation with test()
- Conditional validation with when()
- Integrates with React Hook Form
Next Steps:
- Try Zod
- Use with React Hook Form
- Build Forms
Resources: