JSON Schema で API ペイロードを検証する
JSON Schema が手書きのペイロード検証を宣言的な契約でどう置き換えるか、重要なキーワード、そして人々を悩ませる format の落とし穴を整理します。
手書きの検証は腐ります。if (!body.email) return 400 で始め、長さチェックを足し、
正規表現を足し、先四半期に誰かが足したオプションのフィールドのための分岐を足します。
チェックはドキュメントから漂い、エラーメッセージは一貫せず、誰も思いつかなかった
ケース (age: -1、role: "suuper-admin"、クライアントが決して送るべきでない
さまよいの isAdmin: true) がまっすぐ通過します。JSON Schema はそのすべてを宣言的な
契約で置き換えます。別の JSON ドキュメントが取るべき形を記述する JSON ドキュメント
であり、あなたが手で保守するコードではなく、検証器がチェックします。
作業用スキーマ
ユーザー作成のペイロード用のスキーマです。
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"email": { "type": "string", "format": "email" },
"username": { "type": "string", "minLength": 3, "pattern": "^[a-z0-9_]+$" },
"age": { "type": "integer", "minimum": 13 },
"role": { "type": "string", "enum": ["reader", "author", "admin"] },
"tags": {
"type": "array",
"items": { "type": "string" },
"maxItems": 10
}
},
"required": ["email", "username", "role"],
"additionalProperties": false
}
通過するインスタンスです。
{
"email": "[email protected]",
"username": "lin_99",
"age": 27,
"role": "author",
"tags": ["ml", "rust"]
}
失敗するインスタンスです。
{
"email": "[email protected]",
"username": "Li",
"age": 11,
"role": "superuser",
"isAdmin": true
}
検証器は、違反したキーワードごとにエラーを 1 つ出し、それぞれがインスタンスの正確な 位置を指します。形はライブラリごとに異なりますが、実質は一貫しています。
/username : "Li" は短すぎる (minLength 3)
/username : "Li" がパターン "^[a-z0-9_]+$" に一致しない
/age : 11 が最小値 13 より小さい
/role : "superuser" が ["reader","author","admin"] のいずれでもない
/ : 追加プロパティ "isAdmin" は許可されない
それは 1 つの宣言的なドキュメントから出た、5 つの区別された、位置の指定された エラーです。同等の手書きのコードは、あなたがドキュメントのスキーマと永遠に同期させ なければならない数十行です。
キーワード、グループごとに
JSON Schema はキーワードが多いですが、3 つの仕事に落ちます。
構造 は形を記述します。type が JSON の型 (object、array、string、
number、integer、boolean、null) を制約します。properties がキーをサブ
スキーマに対応づけます。required が存在すべきキーを列挙します。それが非 null を
含意せず、存在だけを含意することに注意してください。items が配列のすべての要素に
サブスキーマを適用します。additionalProperties が properties の外のキーが許可
されるかを制御します。
値の制約 は個々の値を狭めます。minimum/maximum (と排他の変種) が数値を縛り、
minLength/maxLength が文字列を、minItems/maxItems が配列を縛ります。
pattern が文字列に正規表現を適用します。enum が値を固定のリストに制限し、
const がちょうど 1 つの値に固定します。これは "type": { "const": "user.created" }
のような判別子のフィールドに便利です。
合成 はサブスキーマを組み合わせます。allOf がすべての枝の一致を、anyOf が
少なくとも 1 つを、oneOf がちょうど 1 つを要求します。$ref が名前付きのサブ
スキーマを指し、形を一度定義して再利用させ、$defs がその定義を置く慣習的な場所
です。再利用できる住所のブロックはこう見えます。
{
"type": "object",
"properties": {
"billing": { "$ref": "#/$defs/address" },
"shipping": { "$ref": "#/$defs/address" }
},
"$defs": {
"address": {
"type": "object",
"properties": {
"street": { "type": "string" },
"country": { "type": "string", "pattern": "^[A-Z]{2}$" }
},
"required": ["street", "country"]
}
}
}
oneOf はタグ付きユニオンの普通の道具です。メールイベントか SMS イベントの
どちらかであるペイロードを、const のチャネルフィールドで区別するものです。しかし
oneOf は ちょうど 1 つ の一致を強制するので、重なる枝は紛らわしい「1 つより多く
一致する」エラーを生みます。枝が判別子で本当に相互排他なら oneOf が正しく、単に
重なるだけなら、たいてい anyOf が意図したものです。
format の落とし穴
format は人々が最もよく読み違えるキーワードです。"format": "email" を書くと
不正なアドレスを拒否しそうに見え、"format": "date-time" が悪いタイムスタンプを
拒否しそうに見えます。多くの検証器で、既定では、そうしません。format は 注釈、
つまり意図された意味についてのヒントであり、表明は明示的にオンにしない限りオフです。
だから "format": "email" を持つスキーマが "not an email" を喜んで受け入れること
があります。検証器が format をメタデータとして記録して先へ進むからです。format
に実際に値を制約させるには、format の表明をオンにしなければなりません。Ajv の
コンストラクタのフラグ (validateFormats)、2020-12 の format の語彙、またはあなたの
言語のライブラリの同等のオプションです。挙動は実装やドラフトごとに異なるので、安全な
仮定は「format は、私の検証器で働くことを証明するまで何もしない」です。保証が必要
なら、format を常に表明される pattern で裏打ちしてください。
additionalProperties で厳格に
既定では、JSON Schema はあなたが言及しなかったキーを無視します。予期しない
isAdmin: true を運ぶペイロードが、あなたが別に言わない限りきちんと検証されます。
"additionalProperties": false を設定すると、properties に名前のないどのキーも
拒否し、これがスキーマを閉じた契約に変え、usrename のような誤字や、クライアントが
決して送るべきでないフィールドを捕まえます。
トレードオフは前方互換性です。厳格なスキーマは、未来のクライアントのバージョンが 足すかもしれない追加のフィールドを拒否するので、v2 のクライアントが v1 のサーバーと 話すとき、サーバーが安全に無視したはずのプロパティで壊れます。両端を制御する内部の API には、厳格が正しい既定です。未知のキーはバグです。サーバーより先行する クライアントに耐える必要のある公開 API には、開けておくこと (またはあなたが本当に 所有するフィールドに厳格さを限定すること) が、良性の追加で壊れないようにします。
真価を発揮する場所
宣言的な形は、同じ形が 1 か所より多く記述されるところならどこでも真価を発揮します。
- API の境界。 ハンドラが回る前にリクエストを、出荷する前にレスポンスを検証して ください。スキーマが単一の真実のソースになり、あなたが返す 400 が手組みではなく そこから生成されます。
- 設定ファイル。 サービスの設定の上のスキーマが、誤字のキーや範囲外のタイム アウトを午前 3 時ではなく起動時に捕まえます。これは設定が JSON でも YAML でも 当てはまります。同じデータモデルを記述するからです。両者がどこで分かれるかは JSON vs YAML を見てください。
- OpenAPI。 OpenAPI はリクエスト・レスポンスの本文に JSON Schema を埋め込むので、 あなたが書くスキーマが API の仕様としても二役を果たします。
- コード生成。 多くのツールチェーンがスキーマから直接、型やクライアントのコードを 生成し、モデルとコードを歩調を合わせて保ちます。
- エディタの支援。 エディタをスキーマに向けると、設定を手で編集する間にインライン
の検証と補完を得ます。
tsconfig.jsonやpackage.jsonを自己チェックさせるのと 同じ体験です。
ドラフトと、あなたの検証器が対応するもの
JSON Schema はドラフトでバージョン付けされます。2020-12 が最新で、新しい作業で
狙うものです。draft-07 が今もきわめてよく使われます。多くのツールがそこに落ち
着いて決して移らなかったからです。draft-04 が古い OpenAPI 2.0 のスタックに今も
現れます。上記のキーワードは最近のドラフトにわたって安定していますが、細部、つまり
$ref がどう解決されるか、format が表明するか、$defs 対古い definitions の
綴りは、バージョンの間で変わりました。ドキュメントの先頭の $schema の宣言が
ドラフトを名指します。それを尊重し、あなたの検証器が仮定ではなく実際にそのドラフトを
実装するかを確認してください。ここの不一致は、あなたが思うより少なく検証するスキーマを
静かに生みます。
スキーマを先に書き、実際のペイロードに対して早く検証し、契約がそれが守るコードの隣に 宿るようにしてください。ブラウザでスキーマをサンプルのドキュメントに対して下書きして テストするには、私たちの JSON Schema 検証器 がインスタンスを スキーマに対してチェックし、各エラーをその位置とともに報告します。そして貼り付ける前に ペイロードを整えるだけなら、JSON フォーマッタ がまず整形して リントします。