Valibotとは?Zodとの違いとpipe構文の使い方をv1系の実コードで解説
Valibotは、JavaScript/TypeScript向けのスキーマ検証ライブラリです。公式比較ページのログインフォーム検証では、出荷されるコードが1.37kB(bundlejsによるminify+gzip計測)に収まり、同じ検証をZodで書いた場合の17.7kBより約90%小さくなります。バンドルサイズが直接ユーザー体験に響くフロントエンドで、Zodの代替として選ばれています。
ただし、日本語で公開されているValibot記事の多くは2024年前半までに書かれたもので、当時の string([email()]) という書き方を前提にしています。この構文はv0.31.0で廃止され、現在は v.pipe() に置き換わりました。ネットのサンプルをそのまま貼っても動かないのはこのためです。本記事は最新のv1.4.2(2026年6月28日リリース)で動くコードに書き直し、Zod v4/Zod Miniが出そろった今どちらを選ぶべきかまで踏み込みます。
まとめ
- Valibotの正体:小さな関数を
v.pipe()で連結してスキーマを組み立てる、モジュラー設計のスキーマ検証ライブラリ。未使用コードがバンドラのtree-shakingで丸ごと落ちるため、公式比較のログインフォーム検証では1.37kBに収まる。 - 基本API:同期の検証は
v.parse()(失敗時にValiErrorを投げる)、v.safeParse()(.success・.output・.issuesを返す)、v.is()(型ガード)。非同期スキーマにはv.safeParseAsync()など Async 版を使う。フォーム用途では例外を投げないsafeParseが基本。 - Zodとの選び分け:バンドルサイズが要件ならValibot(1.37kB)。エコシステムの広さと書きやすさを取るならZod v4(同条件で17.7kB)。中間のZod Miniでも6.88kBで、Valibotの約5倍(いずれもesbuild計測)。サーバー専用のAPI検証だけならバンドルサイズは効かないので、Zodを捨ててまでValibotに移る理由は薄い。
- 古い記事の罠:
string([email()])やvalidate()はv1では動かない。v0.31.0以降はv.pipe(v.string(), v.email())。移行対応表を本文に用意した。 - ロックインしない:ValibotはStandard Schemaに準拠しているため、React Hook FormやtRPCなど対応ツール側のコードを変えずにZod/ArkTypeと差し替えられる。
Valibotの設計思想と1.37kBの根拠
Valibotは、依存パッケージを持たないスキーマ検証ライブラリです。サーバーのリクエスト検証、フォーム入力、設定ファイルの読み込みなど、外から来る値が期待どおりの形かを実行時に確かめ、同時にTypeScriptの型として確定させる用途で使います。
他ライブラリと決定的に違うのは、機能を「メソッド」ではなく「独立した関数」として提供する点です。Zodでは z.string() が返すオブジェクトに .min()、.email()、.trim() といったメソッドがぶら下がります。この構造だと、使っていないメソッドもオブジェクトの一部として出荷されるため、バンドラが機械的に削り落とせません。Valibotは v.string() と v.email() を別々のimportにし、v.pipe() で連結します。importしていない関数はそもそもバンドルに入りません。
この差が公式の比較ページで数値になっています。同じログインフォームの検証を bundlejs で計測(minify+gzip)した結果は、Valibotが1.37kB、Zodがesbuildで17.7kB・Rolldownで15.18kB。Zodがtree-shaking向けに用意したZod Miniでも、esbuildで6.88kB・Rolldownで3.94kBで、Valibotの3〜5倍です。
採用実績としては、The Guardianが本番で利用しています。GitHubのdependentsは、2025年3月のv1告知時点で5万件超、2026年7月時点では9万8千件を超えました。個人開発者の実験段階のライブラリではありません。
インストールと最小構成
導入手順とバージョンの確認
npmからインストールします。TypeScriptの型定義は本体に同梱されているため、@types/valibot のような別パッケージは不要です。
npm install valibot
# pnpm add valibot / yarn add valibot でも同じ
# 実際に入ったバージョンを確認する
npm list valibot
バージョンは必ず確認してください。v1.0(2025年3月19日リリース)を境にAPIが変わっており、v0系が入っていると本記事のコードは動きません。既存プロジェクトで [email protected] が固定されている場合は、後述の移行章を先に読んでください。
ESMとCommonJSの両方に対応しています。推奨は名前空間import(import * as v from 'valibot')です。Valibotの関数名は string、number、object といった一般的な単語で、個別importするとアプリ側の変数名と衝突しやすいためです。
parse・safeParse・isの使い分け
同期スキーマの検証の入口は3つ。返り値の形が違うので、失敗をどう扱いたいかで選びます。
import * as v from 'valibot';
const EmailSchema = v.pipe(v.string(), v.email());
// 1. parse: 失敗すると ValiError を throw する
const email = v.parse(EmailSchema, '[email protected]'); // string 型として確定
// 2. safeParse: 例外を投げず、結果オブジェクトを返す
const result = v.safeParse(EmailSchema, '[email protected]');
if (result.success) {
console.log(result.output); // 検証を通った値
} else {
console.log(result.issues); // 失敗の詳細(配列)
}
// 3. is: 型ガード。真偽値だけを返す
if (v.is(EmailSchema, input)) {
// このブロック内では input は string 扱い
}
フォームでは safeParse を使ってください。入力ミスは想定内の分岐であって例外ではなく、issues をそのままエラー表示に流せます。API層で「不正なリクエストは即400を返す」といった設計なら、parse の例外をエラーハンドラで捕まえるほうが簡潔になる。
データベース照会のように非同期の検証を挟むスキーマ(v.checkAsync() などを含むもの)では、同期版は使えません。v.parseAsync()/v.safeParseAsync()/v.isAsync() に置き換えてください。
スキーマ定義とpipe構文
オブジェクト・配列・ネストの書き方
v.object() のプロパティに他のスキーマを渡すだけで、いくらでも入れ子にできます。
import * as v from 'valibot';
const UserSchema = v.object({
name: v.pipe(v.string(), v.minLength(1, '名前を入力してください')),
age: v.pipe(v.number(), v.minValue(0), v.maxValue(150)),
email: v.pipe(v.string(), v.email('メールアドレスの形式が不正です')),
tags: v.array(v.string()),
address: v.object({
zip: v.pipe(v.string(), v.regex(/^\d{3}-\d{4}$/)),
city: v.string(),
}),
});
const result = v.safeParse(UserSchema, input);
v.pipe() の第1引数は必ずスキーマ(v.string() など「型」を決めるもの)です。2番目以降には基本的にアクション(v.minLength() など「条件」を課すもの)を並べます。例外は v.transform() で、値の型が変わる場合はその後ろに新しいスキーマを続けられます(後述の CountSchema がその形)。
optionalとnullableの違い
混同しやすい2つですが、許す値が違います。
| 関数 | 許す値 | 使いどころ |
|---|---|---|
v.optional(v.string()) |
string / undefined | キー自体が無くてもよい項目 |
v.nullable(v.string()) |
string / null | DBのNULL列に対応する項目 |
v.nullish(v.string()) |
string / null / undefined | 両方許す場合 |
v.optional(v.string(), 'guest') |
string(未指定なら ‘guest’) | デフォルト値を与える場合 |
APIのレスポンスをそのまま検証するときは、サーバーが「キーを省く」のか「nullを返す」のかで選択が変わります。ここを取り違えると、本番で実データだけが弾かれる不具合になります。
checkとtransformによるカスタム検証(Zodのrefine相当)
Zodの .refine() にあたるのが v.check() です。真偽値を返す関数を渡すだけで、独自ルールを足せます。
import * as v from 'valibot';
// 単一フィールド: 半角英数字のみ
const IdSchema = v.pipe(
v.string(),
v.check((value) => /^[a-zA-Z0-9]+$/.test(value), 'IDは半角英数字のみです')
);
// オブジェクト全体: パスワード一致など複数フィールドにまたがる検証
const SignUpSchema = v.pipe(
v.object({
password: v.pipe(v.string(), v.minLength(8)),
confirm: v.string(),
}),
v.forward(
v.check((input) => input.password === input.confirm, 'パスワードが一致しません'),
['confirm']
)
);
// transform: 検証後に値を変換する(前後の空白を落として数値化)
const CountSchema = v.pipe(v.string(), v.trim(), v.transform(Number), v.number());
v.forward() は、エラーを特定のフィールドに紐づける関数です。これを省くとオブジェクト全体のエラーになり、フォームのどの入力欄に赤字を出せばよいか分からなくなります。パスワード確認のような相互チェックでは必ず併用してください。
スキーマからのTypeScript型推論
スキーマを書けば型は自動で導出されます。型定義とバリデーションを二重管理しないのがこの種のライブラリの本質的な価値です。
type User = v.InferOutput<typeof UserSchema>;
// { name: string; age: number; email: string; tags: string[]; address: { zip: string; city: string } }
type UserInput = v.InferInput<typeof UserSchema>;
// transform 前の型。変換を挟むスキーマでは Output と食い違う
v.transform() を使うと入力型と出力型がずれます。先ほどの CountSchema なら InferInput は string、InferOutput は number です。フォームの入力値の型が欲しいなら InferInput、処理後の値の型が欲しいなら InferOutput を使い分けてください。検証済みの値をアプリ内で「未検証の文字列と区別される型」として扱いたい場合は、Branded Typesによる型の安全性向上と組み合わせると、検証を通していない値の混入をコンパイル時に防げます。
エラー処理とメッセージの日本語化
safeParse が返す issues は配列で、そのままではフォーム表示に使いにくい形です。v.flatten() を通すと、フィールド名をキーにしたオブジェクトへ変換できます。
const result = v.safeParse(UserSchema, input);
if (!result.success) {
const flat = v.flatten<typeof UserSchema>(result.issues);
// flat.nested?.email -> ['メールアドレスの形式が不正です']
// flat.nested?.['address.zip'] -> [...]
// flat.root -> オブジェクト全体に対するエラー
setErrors(flat.nested ?? {}); // React Hook Form 等のフォーム状態へ渡す
}
メッセージの日本語化には3段階の設定関数があり、具体的な指定が優先されます(specific → schema → global の順に探索)。
import * as v from 'valibot';
// 1. すべてのエラーの既定文言
v.setGlobalMessage((issue) => `入力値が不正です: ${issue.received}`);
// 2. 型違反(string を期待して number が来た等)だけの文言
v.setSchemaMessage((issue) => `型が違います: ${issue.expected} が必要です`);
// 3. 特定のアクションだけの文言
v.setSpecificMessage(v.minLength, (issue) => `${issue.requirement}文字以上で入力してください`);
公式の @valibot/i18n パッケージを使えば、翻訳を自前で書かずに読み込めます。言語ごと・関数ごとにimportを分けられるので、使う言語ぶんだけがバンドルされます。
import '@valibot/i18n/ja'; // 日本語の翻訳をすべて登録
import '@valibot/i18n/ja/minLength'; // minLength の翻訳だけ登録
v.setGlobalConfig({ lang: 'ja' }); // 全体の言語を切り替える
v.parse(Schema, input, { lang: 'ja' }); // 呼び出し単位で切り替える
対応言語は追加が続いているため、導入前に公式のi18nガイドで収録言語を確認してください。翻訳が無い言語や、業務固有の言い回しが必要な箇所は setSpecificMessage で個別に上書きするのが現実的です。
ZodとValibotの違いと選び方(Zod v4・Zod Mini時代)
構文の違い:pipe連結とメソッドチェーン
同じ検証を並べると、思想の差がはっきり出ます。
// Valibot
email: v.pipe(
v.string(),
v.nonEmpty('メールアドレスを入力してください'),
v.email('形式が正しくありません')
)
// Zod
email: z.string()
.min(1, 'メールアドレスを入力してください')
.email('形式が正しくありません')
Zodのほうが短く読みやすい、という評価は妥当です。Valibotの冗長さは、tree-shakingを効かせるために払っているコストです。書き味を取るか、出荷サイズを取るかというトレードオフがそのまま構文に現れています。
バンドルサイズの実測値
| ライブラリ | esbuild | Rolldown | Valibot比 |
|---|---|---|---|
| Valibot v1 | 1.37 kB | 基準 | |
| Zod Mini | 6.88 kB | 3.94 kB | 約3〜5倍 |
| Zod(フル) | 17.7 kB | 15.18 kB | 約11〜13倍 |
公式比較ページが bundlejs で計測したログインフォーム検証の値(minify+gzip)です。Valibotは公式が単一値のみを提示しているため、バンドラ別の内訳はありません。実行速度については、Zod v4とValibot v1は実運用レベルのスキーマでは同じ性能帯に入っており、通常のAPI検証で体感差はありません。「Valibotのほうが速いから」という理由での乗り換えは、根拠として弱いと考えてください。差が出るのはサイズです。
どちらを選ぶか(Valibotを選ぶべきでない場面)
判断軸はひとつです。そのスキーマがクライアントのバンドルに載るかどうか。
載るなら、つまりブラウザに配信するフォーム検証・入力チェックなら、Valibotが有利です。1.37kBと17.7kBの差は、低速回線のモバイルや、広告経由のランディングページのように初回描画が直接コンバージョンに響く画面で意味を持ちます。
載らないなら、Valibotに移る理由はほぼありません。Node.jsのAPIサーバー内だけで完結するリクエスト検証では、依存パッケージのサイズは起動時間に微差を生むだけです。この条件下では、既存のZod資産(zod を前提としたtRPC、OpenAPIジェネレータ、各種プラグイン)を捨てるコストのほうが確実に高くつきます。Zod Miniという中間解も出ているため、「サイズが気になるがZodのエコシステムは手放したくない」という要件には、まずZod Miniを試すべきです。
Yup・Joiとの比較を気にしている場合は、判断はもっと単純です。Yupは型推論が後付けで、Joiはそもそも型定義が別管理になります。TypeScriptプロジェクトを新規に始めるなら、ValibotかZod(あるいはArkType)から選べば足ります。
旧API(v0.30以前)の記事が動かない理由と移行対応表
Valibotを検索して出てくる日本語記事の多くは2023〜2024年前半のもので、当時の構文を前提にしています。v0.31.0でAPIが全面的に変わり、検証ルールを配列で渡す書き方は廃止されました。これがValibot最大の落とし穴です。コピーしたコードが型エラーで動かないとき、あなたのTypeScript設定が悪いのではなく、記事が古いだけというケースがほとんどです。
| 旧(v0.30以前) | 現行(v0.31.0以降・v1) |
|---|---|
string([email()]) |
v.pipe(v.string(), v.email()) |
string([minLength(8)]) |
v.pipe(v.string(), v.minLength(8)) |
number([minValue(0)]) |
v.pipe(v.number(), v.minValue(0)) |
custom(fn, msg) |
v.check(fn, msg) |
toTrimmed() |
v.trim() |
Output<typeof S> |
v.InferOutput<typeof S> |
Input<typeof S> |
v.InferInput<typeof S> |
加えて注意点がひとつ。validate(schema, data) という関数はValibotに存在したことがありません。解説記事の中にはこの名前で書かれたものがありますが、生成AIが書いた誤りです。同期の検証の入口は parse/safeParse/is、非同期ならその Async 版であり、validate() はどのバージョンにも存在しません。
対応表で custom(fn, msg) を v.check() に置き換えている点には注意が必要です。v1にも v.custom は存在しますが、これは旧 special() が改名されたもので、任意の型を検証するスキーマ。旧 custom()(=独自ルールを課すアクション)とは役割が別物です。名前が同じでも読み替えないでください。
移行作業自体は機械的です。配列を v.pipe() の可変長引数に開き、先頭にスキーマを置くだけで大半が片付きます。型パズルに悩む前に、まず npm list valibot でバージョンを確認する癖をつけてください。
Standard Schemaによる相互運用とロックイン回避
ValibotはStandard Schemaに準拠しています。これは、Valibotの作者がZod・ArkTypeの作者らと共同で策定した、スキーマ検証ライブラリ共通のインターフェース仕様です。約60行のTypeScript型定義でしかありませんが、効果は大きい。
従来、フォームライブラリやRPCフレームワークは @hookform/resolvers/zod のようにライブラリごとのアダプタを用意する必要がありました。Standard Schemaに対応したツールなら、Valibotのスキーマをそのまま渡せます。React Hook FormのバリデーションやTanStack Formとzodバリデーションのようなフォーム実装で、検証ライブラリだけを後から差し替えられるということです。
実務上の意味は「決め打ちを先延ばしにできる」ことです。バンドルサイズが問題になった段階でZodからValibotへ切り替える、その逆もできる。ライブラリ選定の失敗コストが下がります。
周辺パッケージとしては、ValibotのスキーマからJSON Schemaを生成する公式の @valibot/to-json-schema があります。OpenAPI定義やLLMの構造化出力にスキーマを渡したい場合、Valibotの定義を単一の情報源にできます。TypeScriptそのものの型システムの動向はTypeScript 6.0の新機能と移行手順で整理しています。
v1.3・v1.4で追加された機能
2026年7月時点の最新はv1.4.2(2026年6月28日)。直近2つのマイナーリリースで、実務に効く関数が増えました。
v1.3(2026年3月17日)の追加分。
v.cache()・v.cacheAsync():同じ入力に対する検証結果を再利用する。文字列や数値は値で、オブジェクトや関数は参照でキャッシュされる。重いcheckを含むスキーマをリアルタイム検証で毎打鍵走らせる場面で効く。v.guard():既存の型述語(value is Fooを返す関数)をそのままアクションとして使える。自作のtype guardをValibotのpipeに持ち込める。v.parseBoolean():"true"、"1"、"yes"といった文字列を真偽値へ変換する。環境変数やクエリパラメータの処理で自前の変換関数を書かずに済む。- 新しい検証アクション:
v.domain()(プロトコルやパスを含まないドメイン名)、v.jwsCompact()(JWS Compact Serialization形式)、v.isrc()(国際標準レコーディングコード)。
v1.4(2026年5月5日)の追加分。
- ケース変換アクション:
v.toCamelCase()、v.toSnakeCase()、v.toKebabCase()、v.toPascalCase()。snake_caseで返ってくるAPIレスポンスを、検証と同時にcamelCaseへ寄せられる。従来はv.transform()に自前の変換関数を渡していた部分がそのまま消える。 v.isoDateTimeSecond():秒までを含むISO日時形式の検証。
互換性の修正も入っています。v1.3で v.creditCard() が13桁の旧Visa番号を受け付けるようになり、v.isoTimestamp() がUTCオフセット前の空白を許容。v1.4ではMastercardの桁判定が修正されました。既存スキーマが弾いていた実データが通るようになるため、アップデート時は決済まわりのテストを走らせてください。
よくある質問
ValibotとZodの違いは何ですか?
設計とサイズが違います。Zodはメソッドチェーン(z.string().email())、Valibotは独立関数のpipe連結(v.pipe(v.string(), v.email()))。この差でtree-shakingの効きが変わり、同じログインフォーム検証の公式比較値はValibot 1.37kB、Zod 17.7kB(esbuild)でした。実行速度は現行版どうしならほぼ同等です。
Valibotのバージョンはどこで確認しますか?
npm list valibot で確認できます。v0.31.0未満なら本記事のpipe構文は使えません。v1.0のリリースは2025年3月19日、2026年7月時点の最新はv1.4.2(2026年6月28日)です。
optionalとnullableはどう使い分けますか?
v.optional() は undefined(=キーが無い)を許し、v.nullable() は null を許します。両方許すなら v.nullish()。v.optional(v.string(), 'guest') のように第2引数を渡すとデフォルト値になります。
Zodの refine に相当する機能はありますか?
v.check() です。複数フィールドをまたぐ検証(パスワード一致など)では v.forward() と組み合わせて、エラーを表示したいフィールドに紐づけます。
エラーメッセージを日本語にできますか?
できます。各アクションの第2引数に文字列を渡す方法、v.setSpecificMessage() などで一括設定する方法、公式の @valibot/i18n パッケージを読み込んで v.setGlobalConfig({ lang: 'ja' }) で切り替える方法の3通りです。