Yup
ライブラリ
Yup
概要
YupはJoiにインスパイアされた軽量スキーマバリデーションライブラリです。クライアントサイドバリデーション向けに設計され、FormikやReact Hook Formなどのフォームライブラリとの優れた相性を持ちます。Reactをはじめとするフロントエンドフレームワークでのフォームバリデーションで人気を博し、軽量でシンプルなAPIにより、クライアントサイド開発での生産性向上に大きく貢献しています。
詳細
Yup 1.4.0は2025年現在の最新版で、TypeScript完全対応と軽量性を両立したフロントエンド特化型バリデーションライブラリです。Joiのパワフルなスキーマ定義の考え方を引き継ぎつつ、ブラウザ環境での使用に最適化。React Hook Form、Formik、React Final Formなどの主要フォームライブラリとのシームレスな統合により、モダンなWebアプリケーションでのフォーム開発を効率化します。軽量でありながら強力なバリデーション機能を提供し、ユーザー体験の向上を実現します。
主な特徴
- 軽量設計: フロントエンド向けに最適化された軽量なライブラリサイズ
- フォームライブラリ統合: Formik、React Hook Form等との優れた統合性
- TypeScript対応: 完全なTypeScriptサポートと型推論
- チェイニングAPI: 直感的で読みやすいスキーマ定義方法
- 非同期バリデーション: Promise対応による非同期検証機能
- カスタマイズ性: 豊富なカスタムバリデーションオプション
メリット・デメリット
メリット
- フロントエンドフレームワークでの抜群の使いやすさ
- Formikとの組み合わせによる強力なフォーム開発体験
- 軽量でブラウザパフォーマンスへの影響が最小限
- シンプルで学習しやすいAPI設計
- React以外のフレームワークでも使用可能
- アクティブなコミュニティとメンテナンス
デメリット
- サーバーサイドバリデーションには不向き
- Joiと比較してバリデーション機能が限定的
- 複雑なバリデーションロジックでは制約がある
- Node.js環境での使用はサポート外
- エンタープライズレベルの要件には機能不足
- ドキュメントがJoiほど充実していない
参考ページ
書き方の例
インストールと基本セットアップ
# Yupのインストール
npm install yup
# TypeScript統合(型定義は内蔵)
npm install @types/yup # 通常は不要
# Yarnを使用する場合
yarn add yup
基本的なスキーマ定義とバリデーション
import * as yup from 'yup';
// 基本的なスキーマ定義
const userSchema = yup.object({
name: yup.string()
.min(2, '名前は2文字以上で入力してください')
.max(50, '名前は50文字以下で入力してください')
.required('名前は必須項目です'),
email: yup.string()
.email('有効なメールアドレスを入力してください')
.required('メールアドレスは必須項目です'),
age: yup.number()
.integer('年齢は整数で入力してください')
.min(18, '18歳以上である必要があります')
.max(120, '年齢は120歳以下で入力してください')
.required('年齢は必須項目です'),
website: yup.string()
.url('有効なURLを入力してください')
.nullable(),
agreeToTerms: yup.boolean()
.oneOf([true], '利用規約に同意する必要があります')
.required()
});
// データのバリデーション
const userData = {
name: '田中太郎',
email: '[email protected]',
age: 30,
website: 'https://example.com',
agreeToTerms: true
};
// 同期バリデーション
try {
const validData = userSchema.validateSync(userData);
console.log('バリデーション成功:', validData);
} catch (error) {
console.log('バリデーションエラー:', error.errors);
}
// 非同期バリデーション
async function validateUser(data) {
try {
const validData = await userSchema.validate(data);
console.log('バリデーション成功:', validData);
return validData;
} catch (error) {
console.log('バリデーションエラー:', error.errors);
throw error;
}
}
// プリミティブ型の例
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());
// 各スキーマのテスト
console.log(stringSchema.isValidSync('hello')); // true
console.log(numberSchema.isValidSync(42)); // true
console.log(booleanSchema.isValidSync(true)); // true
高度なバリデーションルールとカスタムバリデーター
// 複雑なオブジェクトスキーマ
const registrationSchema = yup.object({
// ユーザー名の詳細バリデーション
username: yup.string()
.min(3, 'ユーザー名は3文字以上である必要があります')
.max(20, 'ユーザー名は20文字以下である必要があります')
.matches(/^[a-zA-Z0-9_]+$/, 'ユーザー名は英数字とアンダースコアのみ使用可能です')
.required('ユーザー名は必須項目です'),
// パスワードの複雑なバリデーション
password: yup.string()
.min(8, 'パスワードは8文字以上である必要があります')
.matches(
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/,
'パスワードには大文字、小文字、数字を含める必要があります'
)
.required('パスワードは必須項目です'),
// パスワード確認
confirmPassword: yup.string()
.oneOf([yup.ref('password')], 'パスワードが一致しません')
.required('パスワード確認は必須項目です'),
// 配列の検証
hobbies: yup.array()
.of(yup.string().min(2, '趣味は2文字以上で入力してください'))
.min(1, '少なくとも1つの趣味を入力してください')
.max(5, '趣味は5つまで選択可能です'),
// ネストしたオブジェクト
address: yup.object({
street: yup.string().required('住所は必須項目です'),
city: yup.string().required('市区町村は必須項目です'),
postalCode: yup.string()
.matches(/^\d{3}-\d{4}$/, '郵便番号の形式が正しくありません(例: 123-4567)')
.required('郵便番号は必須項目です'),
country: yup.string().default('日本')
}),
// 条件付きフィールド
hasJobExperience: yup.boolean().required(),
jobExperience: yup.string().when('hasJobExperience', {
is: true,
then: (schema) => schema.min(10, '職歴は10文字以上で入力してください').required('職歴は必須項目です'),
otherwise: (schema) => schema.notRequired()
}),
// カスタムバリデーション
birthDate: yup.date()
.max(new Date(), '生年月日は今日以前の日付である必要があります')
.test('age', '18歳以上である必要があります', function(value) {
const cutoff = new Date();
cutoff.setFullYear(cutoff.getFullYear() - 18);
return !value || value <= cutoff;
})
.required('生年月日は必須項目です')
});
// 動的スキーマの例
const createDynamicSchema = (userType) => {
let schema = yup.object({
name: yup.string().required('名前は必須項目です'),
email: yup.string().email().required('メールアドレスは必須項目です')
});
if (userType === 'business') {
schema = schema.shape({
companyName: yup.string().required('会社名は必須項目です'),
taxId: yup.string()
.matches(/^\d{10,13}$/, '法人番号は10-13桁の数字である必要があります')
.required('法人番号は必須項目です')
});
}
if (userType === 'individual') {
schema = schema.shape({
phoneNumber: yup.string()
.matches(/^[0-9-+().\s]+$/, '有効な電話番号を入力してください')
.required('電話番号は必須項目です')
});
}
return schema;
};
// カスタムバリデーション関数
const phoneNumberValidation = yup.string()
.test('phone', '有効な日本の電話番号を入力してください', function(value) {
if (!value) return true; // 空の場合はOK(requiredで別途チェック)
// 日本の電話番号パターン
const patterns = [
/^0[1-9]\d{1,4}-\d{1,4}-\d{4}$/, // 固定電話
/^0[7-9]0-\d{4}-\d{4}$/, // 携帯電話
/^050-\d{4}-\d{4}$/ // IP電話
];
return patterns.some(pattern => pattern.test(value));
});
// 非同期カスタムバリデーション
const uniqueEmailValidation = yup.string()
.email()
.test('unique-email', 'このメールアドレスは既に使用されています', async function(value) {
if (!value) return true;
// APIでの重複チェック(模擬)
try {
const response = await fetch(`/api/check-email?email=${encodeURIComponent(value)}`);
const data = await response.json();
return data.available;
} catch (error) {
console.error('メール重複チェックエラー:', error);
return true; // エラー時は通す
}
});
フレームワーク統合(React Hook Form、Formik等)
// React Hook Formとの統合
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('有効なメールアドレスを入力してください')
.required('メールアドレスは必須項目です'),
password: yup.string()
.min(6, 'パスワードは6文字以上である必要があります')
.required('パスワードは必須項目です'),
rememberMe: yup.boolean()
});
function LoginForm() {
const { register, handleSubmit, formState: { errors } } = useForm({
resolver: yupResolver(loginSchema)
});
const onSubmit = (data) => {
console.log('ログインデータ:', data);
// ログイン処理
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<input
{...register('email')}
type="email"
placeholder="メールアドレス"
/>
{errors.email && <span className="error">{errors.email.message}</span>}
</div>
<div>
<input
{...register('password')}
type="password"
placeholder="パスワード"
/>
{errors.password && <span className="error">{errors.password.message}</span>}
</div>
<div>
<label>
<input {...register('rememberMe')} type="checkbox" />
ログイン状態を保持する
</label>
</div>
<button type="submit">ログイン</button>
</form>
);
}
// Formikとの統合例
import { Formik, Form, Field, ErrorMessage } from 'formik';
const contactSchema = yup.object({
name: yup.string()
.min(2, '名前は2文字以上で入力してください')
.required('名前は必須項目です'),
email: yup.string()
.email('有効なメールアドレスを入力してください')
.required('メールアドレスは必須項目です'),
subject: yup.string()
.min(5, '件名は5文字以上で入力してください')
.required('件名は必須項目です'),
message: yup.string()
.min(20, 'メッセージは20文字以上で入力してください')
.max(500, 'メッセージは500文字以下で入力してください')
.required('メッセージは必須項目です')
});
function ContactForm() {
return (
<Formik
initialValues={{
name: '',
email: '',
subject: '',
message: ''
}}
validationSchema={contactSchema}
onSubmit={(values, { setSubmitting }) => {
console.log('お問い合わせデータ:', values);
// 送信処理
setSubmitting(false);
}}
>
{({ isSubmitting }) => (
<Form>
<div>
<Field type="text" name="name" placeholder="お名前" />
<ErrorMessage name="name" component="div" className="error" />
</div>
<div>
<Field type="email" name="email" placeholder="メールアドレス" />
<ErrorMessage name="email" component="div" className="error" />
</div>
<div>
<Field type="text" name="subject" placeholder="件名" />
<ErrorMessage name="subject" component="div" className="error" />
</div>
<div>
<Field as="textarea" name="message" placeholder="メッセージ" rows={5} />
<ErrorMessage name="message" component="div" className="error" />
</div>
<button type="submit" disabled={isSubmitting}>
送信
</button>
</Form>
)}
</Formik>
);
}
// React Final Formとの統合例
import { Form, Field } from 'react-final-form';
const profileSchema = yup.object({
firstName: yup.string().required('名前は必須項目です'),
lastName: yup.string().required('苗字は必須項目です'),
bio: yup.string().max(200, '自己紹介は200文字以下で入力してください')
});
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('プロフィールデータ:', values)}
validate={validate}
render={({ handleSubmit, form, submitting, pristine, values }) => (
<form onSubmit={handleSubmit}>
<Field name="firstName">
{({ input, meta }) => (
<div>
<input {...input} type="text" placeholder="名前" />
{meta.error && meta.touched && <span className="error">{meta.error}</span>}
</div>
)}
</Field>
<Field name="lastName">
{({ input, meta }) => (
<div>
<input {...input} type="text" placeholder="苗字" />
{meta.error && meta.touched && <span className="error">{meta.error}</span>}
</div>
)}
</Field>
<Field name="bio">
{({ input, meta }) => (
<div>
<textarea {...input} placeholder="自己紹介" />
{meta.error && meta.touched && <span className="error">{meta.error}</span>}
</div>
)}
</Field>
<div>
<button type="submit" disabled={submitting}>
保存
</button>
<button
type="button"
onClick={form.reset}
disabled={submitting || pristine}
>
リセット
</button>
</div>
</form>
)}
/>
);
}
エラーハンドリングとカスタムエラーメッセージ
// 詳細なエラーハンドリング
const productSchema = yup.object({
name: yup.string()
.min(2, '商品名は2文字以上で入力してください')
.max(100, '商品名は100文字以下で入力してください')
.required('商品名は必須項目です'),
price: yup.number()
.positive('価格は正の数値である必要があります')
.min(1, '価格は1円以上である必要があります')
.max(1000000, '価格は100万円以下である必要があります')
.required('価格は必須項目です'),
category: yup.string()
.oneOf(['electronics', 'clothing', 'books', 'food'], '有効なカテゴリを選択してください')
.required('カテゴリは必須項目です'),
description: yup.string()
.min(10, '商品説明は10文字以上で入力してください')
.max(1000, '商品説明は1000文字以下で入力してください')
.required('商品説明は必須項目です')
});
// カスタムエラーハンドリング関数
function handleValidationErrors(error) {
const fieldErrors = {};
const generalErrors = [];
if (error.inner && error.inner.length > 0) {
// 複数のバリデーションエラー
error.inner.forEach(err => {
if (err.path) {
fieldErrors[err.path] = err.message;
} else {
generalErrors.push(err.message);
}
});
} else {
// 単一のバリデーションエラー
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
};
}
// リアルタイムバリデーションの実装
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);
// フィールドレベルでのバリデーション成功
delete this.errors[fieldName];
// 全体スキーマでの検証(依存関係チェック)
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の使用例
const validator = new FormValidator(productSchema);
// 単一フィールドの検証
async function validateProductName(name, allFormData) {
const error = await validator.validateField('name', name, allFormData);
console.log('商品名エラー:', error);
}
// フォーム全体の検証
async function validateProductForm(formData) {
const result = await validator.validateForm(formData);
if (!result.isValid) {
console.log('バリデーションエラー:', result.errors);
}
return result;
}
// 国際化対応エラーメッセージ
const createLocalizedSchema = (locale = 'ja') => {
const messages = {
ja: {
required: 'このフィールドは必須項目です',
email: '有効なメールアドレスを入力してください',
min: '${min}文字以上で入力してください',
max: '${max}文字以下で入力してください'
},
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'
}
};
const currentMessages = messages[locale] || messages.ja;
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)
});
};
// 使用例
const jaSchema = createLocalizedSchema('ja');
const enSchema = createLocalizedSchema('en');
型安全性とTypeScript統合
// TypeScript用の型安全なYupスキーマ
import * as yup from 'yup';
import { InferType } from 'yup';
// スキーマ定義
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(['ja', 'en']).default('ja')
}),
tags: yup.array().of(yup.string()).default([]),
createdAt: yup.date().default(() => new Date()),
lastLogin: yup.date().nullable().default(null)
});
// 型の自動推論
type UserProfile = InferType<typeof userProfileSchema>;
/*
type UserProfile = {
id: number;
username: string;
email: string;
age: number;
isActive: boolean;
preferences: {
theme: "light" | "dark";
notifications: boolean;
language: "ja" | "en";
};
tags: string[];
createdAt: Date;
lastLogin: Date | null;
}
*/
// 型安全なバリデーション関数
async function validateUserProfile(data: unknown): Promise<UserProfile> {
try {
const validatedData = await userProfileSchema.validate(data, {
stripUnknown: true,
abortEarly: false
});
return validatedData; // TypeScript が型を推論
} catch (error) {
if (error instanceof yup.ValidationError) {
throw new Error(`バリデーションエラー: ${error.errors.join(', ')}`);
}
throw error;
}
}
// ジェネリック型を使用したバリデーター クラス
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>>>> {
// 部分的なバリデーション(必須フィールドを一時的に無視)
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 ['予期しないエラーが発生しました'];
}
}
}
// 使用例
const userValidator = new TypeSafeValidator(userProfileSchema);
// React Hookでの型安全な使用例
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 }
}));
}
};
}
// カスタム型安全バリデーター
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>
});
// 使用例:完全に型安全なスキーマ作成
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),
// ... 他のフィールド
});