valibot の使い方

The modular and type safe schema library for validating structural data

v1.3.17.1M/週MITバリデーション
AI生成コンテンツ

この記事はAIによって生成されました。内容の正確性は保証されません。最新の情報は公式ドキュメントをご確認ください。

Valibot の使い方 — モジュラー設計で軽量な型安全バリデーションライブラリ

一言でいうと

Valibot は、モジュラー設計によりツリーシェイキングを最大限に活かし、最小700バイト未満からバンドルサイズを始められる型安全なスキーマバリデーションライブラリです。Zod と似た開発体験を提供しつつ、バンドルサイズを最大95%削減できます。

どんな時に使う?

  1. APIリクエスト/レスポンスのバリデーション — サーバーサイドで受け取るJSONの構造を実行時に検証し、型安全に扱いたい場合
  2. フォームバリデーション — React Hook Form や SvelteKit などのフォームライブラリと組み合わせて、クライアントサイドで入力値を検証する場合
  3. 設定ファイルや環境変数のパースprocess.env や外部設定ファイルの値を型安全に読み込みたい場合

特にバンドルサイズが重要なフロントエンドアプリケーションや、Edge Functions のようなサイズ制約のある環境で真価を発揮します。

インストール

# npm
npm install valibot

# yarn
yarn add valibot

# pnpm
pnpm add valibot

※ 本記事は valibot v1.3.1 時点の情報に基づいています。

基本的な使い方

最も典型的なパターンとして、オブジェクトスキーマを定義してデータをパースする例を示します。

import * as v from 'valibot';

// スキーマ定義
const UserSchema = v.object({
  name: v.pipe(v.string(), v.minLength(1, '名前は必須です')),
  email: v.pipe(v.string(), v.email('有効なメールアドレスを入力してください')),
  age: v.pipe(v.number(), v.minValue(0), v.maxValue(150)),
});

// スキーマから TypeScript の型を推論
type User = v.InferOutput<typeof UserSchema>;
// => { name: string; email: string; age: number }

// バリデーション実行(失敗時は例外をスロー)
const user = v.parse(UserSchema, {
  name: '田中太郎',
  email: 'tanaka@example.com',
  age: 30,
});

console.log(user); // { name: '田中太郎', email: 'tanaka@example.com', age: 30 }

v.pipe() は Valibot の中核的な概念で、スキーマに対してバリデーションや変換のアクション(パイプライン)を連鎖的に適用します。

よく使うAPI

1. safeParse — 例外を投げないバリデーション

import * as v from 'valibot';

const EmailSchema = v.pipe(v.string(), v.email());

const result = v.safeParse(EmailSchema, 'invalid-email');

if (result.success) {
  console.log(result.output); // string 型として利用可能
} else {
  console.log(result.issues); // バリデーションエラーの詳細配列
  // [{ message: 'Invalid email', path: undefined, ... }]
}

parse と異なり例外をスローしないため、エラーハンドリングを制御フローで行いたい場合に適しています。

2. pipe — バリデーション・変換のパイプライン

import * as v from 'valibot';

// 文字列を受け取り → トリム → 小文字化 → メール形式チェック
const NormalizedEmailSchema = v.pipe(
  v.string(),
  v.trim(),
  v.toLowerCase(),
  v.email(),
);

const email = v.parse(NormalizedEmailSchema, '  TANAKA@Example.COM  ');
console.log(email); // 'tanaka@example.com'

pipe はスキーマを第1引数に取り、以降にバリデーションアクションや変換アクションを任意の数だけ連鎖できます。

3. optional / nullable / nullish — オプショナルな値の扱い

import * as v from 'valibot';

const ProfileSchema = v.object({
  nickname: v.optional(v.string()),           // string | undefined
  bio: v.nullable(v.string()),                // string | null
  website: v.nullish(v.string()),             // string | null | undefined
  role: v.optional(v.string(), 'member'),     // デフォルト値付き
});

type Profile = v.InferOutput<typeof ProfileSchema>;
// {
//   nickname?: string | undefined;
//   bio: string | null;
//   website?: string | null | undefined;
//   role: string;  ← デフォルト値があるため undefined にならない
// }

const profile = v.parse(ProfileSchema, { bio: null });
console.log(profile.role); // 'member'

4. array / union / variant — 複合型の定義

import * as v from 'valibot';

// 配列スキーマ
const TagsSchema = v.pipe(
  v.array(v.pipe(v.string(), v.minLength(1))),
  v.minLength(1, 'タグは1つ以上必要です'),
  v.maxLength(10),
);

// ユニオン型(判別なし)
const StringOrNumberSchema = v.union([v.string(), v.number()]);

// 判別付きユニオン(Discriminated Union)
const EventSchema = v.variant('type', [
  v.object({
    type: v.literal('click'),
    x: v.number(),
    y: v.number(),
  }),
  v.object({
    type: v.literal('keydown'),
    key: v.string(),
  }),
]);

type Event = v.InferOutput<typeof EventSchema>;
// { type: 'click'; x: number; y: number } | { type: 'keydown'; key: string }

const event = v.parse(EventSchema, { type: 'click', x: 100, y: 200 });

variant は判別フィールドを指定することで、union よりも効率的かつ正確にバリデーションを行います。

5. flatten — エラーメッセージの整形

import * as v from 'valibot';

const FormSchema = v.object({
  email: v.pipe(v.string(), v.email('有効なメールアドレスを入力してください')),
  password: v.pipe(
    v.string(),
    v.minLength(8, 'パスワードは8文字以上必要です'),
    v.regex(/[A-Z]/, '大文字を1文字以上含めてください'),
  ),
});

const result = v.safeParse(FormSchema, { email: 'bad', password: 'short' });

if (!result.success) {
  const flat = v.flatten(result.issues);
  console.log(flat);
  // {
  //   nested: {
  //     email: ['有効なメールアドレスを入力してください'],
  //     password: ['パスワードは8文字以上必要です', '大文字を1文字以上含めてください'],
  //   }
  // }
}

flatten はネストされた issues 配列をフィールド名ごとのメッセージ配列に変換してくれるため、フォームUIへのエラー表示に便利です。

類似パッケージとの比較

特徴ValibotZodYupArkType
バンドルサイズ(min+gzip)~1 kB〜(使用分のみ)~14 kB~15 kB~5 kB
ツリーシェイキング◎(モジュラー設計)△(クラスベース)
TypeScript 型推論
API スタイル関数ベース + pipeメソッドチェーンメソッドチェーン文字列ベースDSL
エコシステム成熟度○(急成長中)◎(デファクト)◎(歴史あり)
依存関係00数個0

Zod からの移行を検討する場合: API の概念は似ていますが、Zod のメソッドチェーン(z.string().email())が Valibot では関数合成(v.pipe(v.string(), v.email()))になる点が最大の違いです。公式に Migration Guide が用意されています。

注意点・Tips

ツリーシェイキングの恩恵を最大化する

// ✅ 推奨:名前付きインポートでも動作するが、名前空間インポートが公式推奨
import * as v from 'valibot';

// ✅ これも OK
import { object, string, parse } from 'valibot';

どちらの書き方でもツリーシェイキングは有効です。import * でも未使用の関数はバンドルから除外されます。

pipe の順序に注意

import * as v from 'valibot';

// ✅ 正しい:変換 → バリデーションの順序
const Schema = v.pipe(v.string(), v.trim(), v.minLength(1));

// ⚠️ 意図しない結果:バリデーション → 変換の順序だと
// 空白のみの文字列が minLength(1) を通過してしまう
const BadSchema = v.pipe(v.string(), v.minLength(1), v.trim());

pipe 内のアクションは左から右へ順に実行されます。変換(trim, toLowerCase など)をバリデーションの前に配置するのが一般的なベストプラクティスです。

InferInputInferOutput の使い分け

import * as v from 'valibot';

const Schema = v.object({
  createdAt: v.pipe(v.string(), v.isoTimestamp(), v.transform((s) => new Date(s))),
});

// 入力側の型(変換前)
type Input = v.InferInput<typeof Schema>;
// { createdAt: string }

// 出力側の型(変換後)
type Output = v.InferOutput<typeof Schema>;
// { createdAt: Date }

transform を使う場合、入力と出力で型が異なります。フォームの入力値には InferInput、パース後のデータには InferOutput を使い分けてください。

React Hook Form との統合

import { useForm } from 'react-hook-form';
import { valibotResolver } from '@hookform/resolvers/valibot';
import * as v from 'valibot';

const LoginSchema = v.object({
  email: v.pipe(v.string(), v.email()),
  password: v.pipe(v.string(), v.minLength(8)),
});

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

  return (
    <form onSubmit={handleSubmit((data) => console.log(data))}>
      <input {...register('email')} />
      {errors.email && <span>{errors.email.message}</span>}
      <input type="password" {...register('password')} />
      {errors.password && <span>{errors.password.message}</span>}
      <button type="submit">ログイン</button>
    </form>
  );
}

@hookform/resolvers パッケージが Valibot をサポートしているため、追加設定なしで統合できます。

まとめ

Valibot は、Zod と同等の型安全なバリデーション体験を提供しつつ、モジュラー設計によりバンドルサイズを劇的に削減できるライブラリです。pipe による関数合成スタイルに慣れれば、柔軟かつ読みやすいスキーマ定義が可能になります。バンドルサイズを気にするフロントエンドプロジェクトや、Edge Runtime のような制約のある環境では、Zod からの乗り換え先として有力な選択肢です。

比較記事