最終更新: a few seconds ago Remove Netlify-related code from main (grafted, HEAD)
データのバリデーション
JSON Schema について
入出力時の型チェックに JSON Schema を積極的に活用する。バリデータの実装としては AJV を使用する。
Understanding JSON Schema: https://spacetelescope.github.io/understanding-json-schema/
(シンプルで読みやすく、かつ十分に網羅的なのでお勧め)
AJV: https://ajv.js.org/
バリデータは以下の場面で 半自動的に 使用されるようにする。
- ユーザからの API 呼び出し時のリクエスト内容チェックと型の変換
- DB に何か入力する際の型チェック
- DB からデータを受け取る際の型チェック
- ユーザに API からデータを返す際の内容チェック
MongoDB は入出力時に型をチェックする機能が乏しいため、スキーマが確実に統一されるように保証することはコーディングする側の責任。DB の内容が壊れることを防ぐため、 あらゆる DB への入出力の際に JSON Schema による型チェックが強制される ようにする。
アプリケーション内では AJV のインスタンスを直接作成したり利用したりするのではなく、これをラップしたバリデータを createValidator() により作成し、これ経由でバリデーション機能を利用する。ルートハンドラ内のどこからでも ctx.validator でアクセスできる(createApp() の実装を参照)が、できるだけ手動で呼ぶことがないようにする。
開発途中でスキーマに変更があった場合はそれに対応するマイグレーションを作成する。
スキーマファイル
すべての JSON Schema は src/schemas ディレクトリ配下に配置する。このディレクトリ配下の *.yaml ファイルはスキーマファイルであり、サーバ起動時に createValidator() によりバリデータが作成される際に、一度だけ走査されて読み込まれる。
スキーマファイルは(YAML で書かれていることを除けば)基本的には素直な JSON Schema だが、以下の条件を満たすようにすること。
- ルートは
type: 'object'であること(オブジェクト以外のものにマッチするスキーマを書かないこと)。 $id(ないしid) フィールドを含まないこと。スキーマの ID はファイル名から決定する。(例:user.yamlのスキーマの ID はuser)$async: trueを加えること。拡張性を確保するため、すべてのバリデーションは非同期的に行うことにする。(例え同期的に処理が可能なものであっても。)- ルートレベルにて
requiredキーワードは使用しないこと。これは下記の通り、特別に処理されるため。
スキーマの例 (sample.yaml):
$schema: 'http://json-schema.org/schema#'
$async: true
description: This is a sample schema with one number prop and one string prop.
type: object
properties:
intVal:
type: number
default: 5
strVal:
type: string
default: 'biscuit'
dateVal:
date: true
バリデータの使用法
バリデータのシグネチャは以下の通り。
await validator.validate(schema, data, options);
schema はスキーマ文字列ないし JSON Schema オブジェクト自体、data はバリデーションを行うデータ自体、options は後述するオプション。
手動でバリデータを呼び出す場合は以下のようにする。
// 1. バリデーションエラー時にデフォルトのエラーハンドラーに処理を任せる場合
await validator.validate('sample', data);
// 2. バリデーションエラーを自前で処理する場合
try {
await validator.validate('sample', data);
} catch (errors) {
// errors の内容は AJV のドキュメント参照
console.log(errors);
}
// 3. すべてのフィールドを省略不可能なものとしてバリデーションする場合
await validator.validate('sample', data, { allRequired: true });
// 4. 欠けているプロパティに対してデフォルト値を埋める場合
const filled = validator.validate('sample', data, { fillDefaults: true });
assert.deepEqual(filled, { intVal: 123, strFVal: 'biscuit' }); // pass
すべてのバリデーションは非同期的に行われる。バリデーションにエラーがあった場合は Promise が reject される。必要なら直接 catch してもよい(上記 2 番目の例)が、基本的には catch せずに上流のエラーハンドラーに処理を任せてよい。エラーハンドラーはバリデーションのエラー (Ajv.ValidationError) であることを検出すると、そのエラー内容を正しくフォーマットして JSON で返す。
以下のように、バリデータに渡されるデータ data はバリデーション後に書き換わっている可能性がある。
- スキーマで定義されていないプロパティが自動的に削除される
- 日付の自動変換が行われる可能性がある
従って不変性が重要なデータを validator に直接渡さないように注意すること。
スキーマのキー(上記 sample)の代わりに JSON Schema のオブジェクト自体を渡すことも可能。
await validator.validate(
{ $async: true, type: 'string', format: 'email' },
'a@example.com'
);
バリデータのインスタンスは ctx に注入されるので、ルートのハンドラ内からは ctx.validator でアクセスできる。
async function hanldeGet(ctx, next) {
// If validation fails, an async exception is thrown,
// which will be handled by the upstream error handler
await ctx.validator.validate('user', { name: 'John' });
}
ただしこれを手動で呼び出すのではなく、API マニフェストファイル内で requestSchema, responseSchema を指定し、自動でチェックさせることが望ましい。
オプション
- allRequired
- 指定されている場合、すべてのフィールドを必須なものとしてバリデーションする。これを指定しない場合はすべてのフィールドはオプションとなり、そのフィールドが存在する場合にのみその型をチェックする。
- allRequiredExcept (string[])
- プロパティの文字列の配列を渡すと、それ以外のフィールドを必須なものとしてバリデーションする。
- fillDefaults
- 指定されている場合、スキーマファイルに書かれているデフォルト値でデータを埋める。これが指定されない場合は、スキーマファイルに
defaultが指定されてあっても無視される。 - toDates
- 指定されている場合、
date: trueとなっているプロパティに関して、ISO 日付文字列から Date 型への変換を行う。ISO 日付文字列でない場合はバリデーションエラーとなる。 - fromDates
- 指定されている場合、
date: trueとなっているプロパティに関して、Date 型から ISO 日付文字列への変換を行う。Date 型でない場合はバリデーションエラーとなる。 - dbEntry
- MongoDB のドキュメントを確認する場合のモードでバリデーションを行う。すべてのプロパティが存在していることと、
createdAt/updatedAtのフィールドが存在していることを確認する。
拡張キーワード date と時刻型の自動変換
スキーマファイル中で date: true というキーワードを書くと特殊な動作をする。(JSON Schema にはない独自拡張)
toDate: trueのオプションを付けてバリデータを呼び出した場合、値は ISO 日付文字列であることが期待され、それ以外の場合はバリデーションエラーとなる。バリデーション通過後に、該当の値はDateオブジェクトに自動的に変換されている。これは API リクエストをバリデーションする際に有効。fromDate: trueのオプションを付けてバリデータを呼び出した場合、値はDate型のインスタンスであることが期待され、それ以外の場合はバリデーションエラーとなる。バリデーション通過後に、該当の値は ISO 日付文字列に変換される。これは API のレスポンスにバリデーションをかける場合に有効であり、手作業で DB からの日付型データを変換しないでも済むようにする。- 上記いずれも指定せずに
validate()を呼び出した場合、date: trueなプロパティの値はDate型のインスタンスであることが期待される。これは主にデータベースに登録されているドキュメントの内容をバリデーションする際に有効。
removeAdditional
バリデータの removeAdditional オプションは常に有効となっている(AVJ ドキュメント参照)ため、validate を通すと、テストされるオブジェクトから、スキーマに記載されていないプロパティが自動的に削除される。
❓ additionalProperties: false を強制にして、知らないプロパティが渡されたら問答無用でエラーを返す方が親切かもしれない
特殊な文字列フォーマット
文字列の format としては、 AJV のデフォルトの email などが使えるほか、以下のものが利用可能となっている。他にもいろいろ追加する場合は createValidator を書き換えること。
dicomUid'111.222.3333'のような DICOM の UID 文字列(64 バイト以内の数字・ピリオドの組み合わせ)にマッチ。multiIntegerRange1-5,7,10-15のような整数レンジのコンマ区切り文字列。multi-integer-rangeNPM パッケージにて.toString()で出力されるものと同様。