JSON Schema で API ペイロードを検証する

JSON Schema が手書きのペイロード検証を宣言的な契約でどう置き換えるか、重要なキーワード、そして人々を悩ませる format の落とし穴を整理します。

手書きの検証は腐ります。if (!body.email) return 400 で始め、長さチェックを足し、 正規表現を足し、先四半期に誰かが足したオプションのフィールドのための分岐を足します。 チェックはドキュメントから漂い、エラーメッセージは一貫せず、誰も思いつかなかった ケース (age: -1role: "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 の型 (objectarraystringnumberintegerbooleannull) を制約します。properties がキーをサブ スキーマに対応づけます。required が存在すべきキーを列挙します。それが非 null を 含意せず、存在だけを含意することに注意してください。items が配列のすべての要素に サブスキーマを適用します。additionalPropertiesproperties の外のキーが許可 されるかを制御します。

値の制約 は個々の値を狭めます。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.jsonpackage.json を自己チェックさせるのと 同じ体験です。

ドラフトと、あなたの検証器が対応するもの

JSON Schema はドラフトでバージョン付けされます。2020-12 が最新で、新しい作業で 狙うものです。draft-07 が今もきわめてよく使われます。多くのツールがそこに落ち 着いて決して移らなかったからです。draft-04 が古い OpenAPI 2.0 のスタックに今も 現れます。上記のキーワードは最近のドラフトにわたって安定していますが、細部、つまり $ref がどう解決されるか、format が表明するか、$defs 対古い definitions の 綴りは、バージョンの間で変わりました。ドキュメントの先頭の $schema の宣言が ドラフトを名指します。それを尊重し、あなたの検証器が仮定ではなく実際にそのドラフトを 実装するかを確認してください。ここの不一致は、あなたが思うより少なく検証するスキーマを 静かに生みます。

スキーマを先に書き、実際のペイロードに対して早く検証し、契約がそれが守るコードの隣に 宿るようにしてください。ブラウザでスキーマをサンプルのドキュメントに対して下書きして テストするには、私たちの JSON Schema 検証器 がインスタンスを スキーマに対してチェックし、各エラーをその位置とともに報告します。そして貼り付ける前に ペイロードを整えるだけなら、JSON フォーマッタ がまず整形して リントします。