JSON Schema と Ajv を使ったバリデーションの勘所
Gaji-Labo フロントエンドエンジニアの茶木です。
スタートアップのプロダクト開発支援をしている私たちは、ユーザー体験を損なわないバリデーションのあり方や、プロダクト開発の先々を見据えた実装をしたいと考えています。
はじめに
本記事では JSON Schema バリデーターである Ajv を使ったバリデーションについて、実践的な勘所について共有したいと思います。
JSON Schema は単なるJSON なので 必ずしもコードベースに含める必要がなく、API などから取得し、動的に input フィールドを作成するケースなどでも役に立ちます。
JSON Schema とは?
JSON Schema は値の妥当性のルールをJSON 形式で定義する書式です。
以下は、その JSON ドキュメントを JavaScript 用にパースしたものです。
const schema = {
type: "string",
pattern: "^[0-9-()]+$",
errorMessage: {
pattern: "半角の数字・ハイフン・括弧が利用できます",
},
}
この例では 正規表現判定である pattern
による妥当性判定が指定されています。他にも文字列長を判断する minLength
, maxLength
などがあります。また type
の種類によって使用できるルールも変わります。
基本例
Ajv を使ってバリデータを生成します。
import Ajv from "ajv";
import AjvErrors from "ajv-errors";
const ajv = new Ajv({ allErrors: true });
AjvErrors(ajv, { singleError: false });
const validator = ajv.compile(schema);
// OK
const goodResult = validator.validate("123-456-789");
console.log(goodReuslt); // true
// NG
const badResult = validator.validate("XYZ");
console.log(badResult, validator.errors[0].message); // false, "半角の数字・ハイフン・括弧が利用できます"
前述の JSON Schema を compile
に渡すとバリデーター( validator
)が生成されます。
validator.validate
で値のバリデーションが行えます。バリデーションに失敗しエラーがあるときは、 validator.erros
にエラーの内容が記載されます。
実際の環境で使うときの勘所
object で管理する
基本例では単一の値についてバリデーションを行いましたが、実際の環境では、複数の値がバリデーションの対象になるでしょう。
type: object
を指定し properties
にキーを記載しネスト構造にできます。
const schema = {
type: "object",
required: [foo, bar],
properties: {
foo: {
type: "string",
pattern: "^[0-9-()]+$",
errorMessage: {
pattern: "半角の数字・ハイフン・括弧が利用できます",
},
},
bar: {
type: "string",
maxLength: 10,
errorMessage: {
maxLength: "10文字以内で入力してください",
},
}
}
}
errorMessage の抽出
validator
が持つ errors
は、エラーの発生条件や発生箇所など多くの情報を持ちますが、実際のフォーム上にエラーメッセージを表示するには、対応する キー名とエラーメッセージのペアが取得できれば十分なので errors
をラップして使いやすくします。
function validate(data): {valid: true} || { valid: false, errorMessages: Record<string, string>} {
if( validator.validate(data) ) {
return { valid: true }
}
const errorMessages = validator.errors.reduce((acc, elm) => {
if (
"instancePath" in elm &&
typeof elm.instancePath === "string" &&
elm.message
) {
// NOTE: instancePathの形式は /foo のようになるため先頭の/を削除
const name = elm.instancePath.slice(1);
return { ...acc, [name]: elm.message };
}
return acc;
}, {});
return { valid: false, errorMessages };
}
ポイントは instancePath
に /
区切りでキー名が含まれることを考慮して、キー名とメッセージをペアにすることです。
つまづきポイント required
const schema = {
type: "object",
required: [foo, bar],
properties: {
foo: { ... },
bar: { ... }
}
errorMessage "未入力の項目があります"
}
ここで一点、つまづきやすいポイントについて解説します。
required
は type: object
に設定できるバリデーションルールですが、通常フロントエンドで想定される required
とは若干の違いがあります。
required
のエラーはobject
要素に対して発生し、各要素ではエラーが発生せず、errorMessage
も 各要素に指定できない。required
でエラーとなるのはundefined
。空文はエラーにならない。
ようするに JSON Schema の required
は子要素の存在をチェックするものなのですが、通常フロントエンドで想定する required
はテキストが空文でないことが条件なのです。
代替方法は minLength: 1
や not: const: ""
を子要素に指定することです。これで、空文をエラーとすることができます。
おわりに
ユーザーインプットとバリデーションは切り離せないものです。バリデーションはプロダクトの安全性を担保します。一方で、提供の仕方次第ではユーザーにとっては煩わしいものにもなりえます。
Gaji-Labo は安全性もユーザー体験も大切に考えて、スタートアップのプロダクト支援を行っていきます。
Gaji-Labo フロントエンドエンジニア向けご案内資料
Gaji-Labo は Next.js, React, TypeScript 開発の実績と知見があります
フロントエンド開発の専門家である私たちが御社の開発チームに入ることで、バックエンドも含めた全体の開発効率が上がります。
「既存のサイトを Next.js に移行したい」
「人手が足りず信頼できるエンジニアを探している」
「自分たちで手を付けてみたがいまいち上手くいかない」
フロントエンド開発に関わるお困りごとがあれば、まずは一度お気軽に Gaji-Labo にご相談ください。
オンラインでのヒアリングとフルリモートでのプロセス支援にも対応しています。
Next.js, React, TypeScript の相談をする!