最終更新: a few seconds agoRemove Netlify-related code from main (grafted, HEAD)

データのバリデーション

JSON Schema について

入出力時の型チェックに JSON Schema を積極的に活用する。バリデータの実装としては AJV を使用する。

バリデータは以下の場面で 半自動的に 使用されるようにする。

  1. ユーザからの API 呼び出し時のリクエスト内容チェックと型の変換
  2. DB に何か入力する際の型チェック
  3. DB からデータを受け取る際の型チェック
  4. ユーザに 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 バイト以内の数字・ピリオドの組み合わせ)にマッチ。
multiIntegerRange
1-5,7,10-15 のような整数レンジのコンマ区切り文字列。 multi-integer-range NPM パッケージにて .toString() で出力されるものと同様。