Yup

バリデーションライブラリJavaScriptReactフォームクライアントサイドFormik

ライブラリ

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),
  // ... 他のフィールド
});