はじめに
フォームの入力チェックを書いていると、「Aを選んだときだけBが必須になる」「配列の中身もオブジェクトとして検証したい」といった条件分岐だらけのバリデーションに行き着くことがよくあります。単純なif文の積み重ねで対応していくと、ルールが増えるたびにコードが読みにくくなり、どこで何をチェックしているのか把握しづらくなってしまいます。
Joiは、この手のバリデーションを宣言的なスキーマとして書けるライブラリです。hapi.jsチームが開発しており、Node.jsのバリデーションライブラリの中でも老舗にあたりますが、.when()による条件付きルールやカスタムエラーメッセージなど、複雑な要件になるほど威力を発揮する機能がそろっています。
実際に手を動かして挙動を確かめたい方は、先にサンプルをどうぞ。
Joiとは
Joi(joi.dev)は、JavaScript向けのスキーマ記述言語兼データバリデーションライブラリです。GitHubリポジトリ(hapijs/joi)は21,000以上のスターを持ち、hapi.jsフレームワークのエコシステムの一部として開発されてきましたが、単体でも幅広いプロジェクトで使われています。
主な特徴
- メソッドチェーンで書けるスキーマ定義 -
Joi.string().min(3).max(30).required()のように、制約を積み重ねる形で直感的にルールを表現できます .when()による条件付きバリデーション - あるフィールドの値によって、別フィールドの必須・禁止を切り替えるルールを1つのスキーマ内で表現できます- 詳細なエラー情報 - どのフィールドの何のルールに違反したかが
error.detailsに構造化されて返ってくるため、フォームの各項目にエラーを紐づけやすい設計です - カスタムメッセージと拡張 -
.messages()でエラー文言を差し替えられるほか、Joi.extend()で独自のバリデーションルールを追加できます - Node.jsとブラウザの両対応 - サーバー側のリクエスト検証にもクライアント側のフォーム検証にも同じスキーマ定義を使い回せます
インストール
npmまたはyarnでインストールします。
npm install joi
yarn add joi
Joiのサンプルを動かす
以下は、ユーザー名とメールアドレスを持つオブジェクトに対してJoi.object()でスキーマを定義し、schema.validate()で検証する最小構成です。フォームに文字を入力するたびに検証が走り、違反しているルールがエラーメッセージとして表示されます。
const schema = Joi.object({
username: Joi.string().alphanum().min(3).max(30).required(),
email: Joi.string().email({ tlds: { allow: false } }).required(),
})
const { error, value } = schema.validate(
{ username: 'ab', email: 'not-an-email' },
{ abortEarly: false } // すべてのエラーをまとめて取得する
)
// error.details に違反したルールの一覧が入る
console.log(error.details.map((d) => d.message))
実際に動かせるサンプルはこちらです。ユーザー名を2文字以下にしたり、メールアドレスを@なしで入力したりすると、どのルールに違反したかがその場で表示されます。
schema.validate()は{ error, value }を返し、問題がなければerrorはundefinedになります。abortEarly: falseを指定すると、最初のエラーで止まらずすべての違反をまとめて取得できるので、フォームの各項目にエラーを同時に表示したいときに便利です。ユーザー名を1文字にする、メールアドレスから@を消すなど、実際に壊してみるとerror.detailsの中身がどう変わるか確認できます。
基本的な使い方
Joiの基本形は「スキーマを定義する」「validate()に値を渡す」の2ステップです。オブジェクトの各キーに対して、型(string()、number()、boolean()など)と制約をメソッドチェーンで指定します。
const Joi = require('joi')
const schema = Joi.object({
name: Joi.string().min(1).max(100).required(),
age: Joi.number().integer().min(0).max(150),
role: Joi.string().valid('admin', 'member', 'guest').default('member'),
})
const { error, value } = schema.validate({ name: 'Sato', age: 28 })
if (error) {
console.error(error.details)
} else {
console.log(value) // { name: 'Sato', age: 28, role: 'member' }
}
valid()で許可する値を列挙したり、default()で未指定時のデフォルト値を設定したりできる点も、Joiが単なる型チェックにとどまらない理由の一つです。Expressなどのサーバーで使う場合は、リクエストボディをそのままschema.validate(req.body)に渡すのが定番のパターンです。
実践的なユースケース
条件によってルールを切り替える(.when())
「法人アカウントを選んだときだけ会社名を必須にする」といった、フィールド同士が依存するバリデーションはフォームでよくある要件です。Joiの.when()を使うと、この分岐を1つのスキーマ定義の中で完結させられます。
const schema = Joi.object({
type: Joi.string().valid('personal', 'company').required(),
companyName: Joi.string().when('type', {
is: 'company',
then: Joi.string().required(),
otherwise: Joi.forbidden(), // personalのときは指定不可
}),
})
typeに応じて、開いた瞬間から会社名欄の必須・任意が切り替わります。「法人」を選んで会社名を空のまま送ろうとするとエラーになり、「個人」を選んでいる状態で会社名を入力するとエラーになる様子を確認できます。
isで条件、then/otherwiseでそれぞれの場合のルールを指定するのが.when()の基本形です。参照するフィールド名をtypeから別のキーに変えたり、then側のルールをrequired()からmin(2)のような別の制約に変えたりすると、依存関係の表現力の高さが分かります。
エラーメッセージを日本語化する(.messages())
Joiの初期状態のエラーメッセージは英語です。ユーザー向けの画面にそのまま出すと不親切なので、.messages()でルールごとにメッセージを差し替えます。{#label}のようなプレースホルダを使うと、項目名を埋め込んだ文言にできます。
const schema = Joi.string()
.min(3)
.required()
.messages({
'string.min': '{#label}は3文字以上で入力してください',
'string.empty': '{#label}は必須です',
})
.label('ニックネーム')
.label()でフィールドの表示名を指定しておくと、{#label}の部分がその名前に置き換わります。以下のサンプルでは、ニックネーム欄を空にしたり短くしたりすると、日本語化したエラー文が表示されます。
messages()のキーは{型}.{ルール名}の形式で指定します。どのルールがどのキーに対応するかはJoiのエラーコード一覧で確認できますが、string.minやstring.emptyのような命名規則が分かれば、他のルールにも応用できます。
ネストした配列を検証する
フォームによっては、1つのオブジェクトの中に配列やネストしたオブジェクトが含まれることがあります。JoiではJoi.array().items()の中にさらにJoi.object()を渡すことで、配列の各要素の構造まで検証できます。
const schema = Joi.object({
users: Joi.array()
.items(
Joi.object({
name: Joi.string().required(),
age: Joi.number().integer().min(0).required(),
})
)
.min(1)
.required(),
})
// error.details[0].path が ['users', 0, 'age'] のように
// どの要素の何が違反したかを教えてくれる
以下のサンプルでは、ユーザーの一覧をJSON形式のテキストエリアで編集し、users配列の各要素がnameとageのルールを満たしているかをまとめて検証します。年齢をマイナスにしたり、nameを空にしたりすると、どの要素の何が問題かが表示されます。
エラーのpathには['users', 1, 'name']のように配列インデックスも含まれるため、どの要素のどのフィールドが違反したのかをそのままUIに反映できます。ネストが深くなっても検証ロジック自体はitems()を重ねるだけなので、フォーム側のコードを複雑にせずに済みます。
まとめ
Joiは、単純な型チェックだけでなく、.when()による条件付きバリデーション、.messages()によるエラー文言のカスタマイズ、配列やネストしたオブジェクトの検証まで、宣言的なスキーマ定義の中で表現できるライブラリです。ルールが増えてif文が絡み合ってきたと感じたら、Joiのスキーマに置き換えることで、検証ロジックの見通しを保ちやすくなります。まずは手元のフォームの1つを、Joiのスキーマとして書き直してみてはいかがでしょうか。
