JSON Schema Draft v.4の規格書を読む

2016年1月5日現在において、JSONを受け取り、返却するWeb APIを書くときに、人が作った規格に乗って楽をしようぜと考えた。

その過程で調べた、JSON Schemaについてメモ書き。間違ってたらツッコミよろ。

概要

JSONの構造を記述する規格。構造の記述そのものもJSONで書かれる。

Draft v4現在では、JSON Schemaは以下の3つの規格の総体を指す。

そもそも提案された初期のJSON Schemaは、JSON Schema Core+JSON Schema Validationとほぼ同じ領域をカバーしていた。整理・発展の上3仕様に分割された。よって、JSON Schema Core+JSON Schema Validationにあたるものを単にJSON Schemaと呼ぶ場合がある。注意。

また、規格そのものではなく、この規格に基づいて書かれたJSONそのものもJSON Schemaと呼ばれる場合がある。まぎらわしいね!

JSON Schema Core

JSONの構造と、各要素の型を指定できる。

ブログのエントリを表すJSONを考えよう。ブログは複数のエントリからなり、エントリは複数のコメントを持つとする。そのようなJSONの実例を示す。

{
  "entries": [
    {
      "id": 1,
      "title": "JSON Schema学んでみた",
      "name": "tasuku",
      "email": "tasuku-s-github@titech.ac",
      "icon": "http://blog.wktk.co.jp/favicon.ico",
      "image": "http://blog.wktk.co.jp/assets/image/json-schema.jpg",
      "text": "仕様書嫁",
      "comments": [
        {
          "id": 101,
          "name": "通りすがり",
          "email": "anonymous@example.com"
          "text": "もっと勉強しましょう。"
        },
        {
          "id": 102,
          "name": null,
          "email": null,
          "text": "エントリ長すぎでは"
        }
      ]
    },
    {
      "id": 2,
      "title": "ひとり暮らしの男性に、電気フライヤーのススメ",
      "name": "tasuku",
      "email": "tasuku-s-github@titech.ac",
      "icon": "http://blog.wktk.co.jp/favicon.ico",
      "image": "http://blog.wktk.co.jp/assets/image/electric-frier.jpg"
      "text": "http://blog.wktk.co.jp/archives/136 を百万遍嫁。",
      "comments": [
      ]
    }
  ]
}

このようなJSONの構造に合致するJSON Schema(Coreの範囲内)の一例を示す。

{
  "type": "object",
  "properties": {
    "entries": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "title": {
            "type": "string"
          },
          "name": {
            "type": ["string", "null"]
          },
          "email": {
            "type": ["string", "null"]
          },
          "icon": {
            "type": "string"
          },
          "image": {
            "type": "string"
          },
          "text": {
            "type": "string"
          },
          "comments": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer"
                },
                "name": {
                  "type": ["string", "null"]
                },
                "email": {
                  "type": ["string", "null"]
                },
                "text": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    }
  }
}

まあ、だいたい見たまんまというか、JSONの構造どおりに入れ子になって型が定義されているイメージ。Nullableな値は、null型もとりますよ、という指定を行う。

以下は、Core範囲内の細かい仕様。

  • $から始まるプロパティ
    • $schema: JSON Schemaのバージョンを定義
    • $ref: スキーマ再利用のための参照。
    • #のあとが/から始まるJSON Pointerであれば、JSON Pointerが指すものを参照する
      • JSON Pointerとは、XMLでいうXPathみたいなもの
      • ex.) #/properties/entries
    • #のあとがJSON Pointerでなければ、ローアルなSchemaの中でIDが一致するものを参照する
      • ex.) #entries
  • idというプロパティ
  • definitionsというプロパティ
    • スキーマのテンプレ置き場
    • $refで参照できる
    • が、スキーマ定義そのものではない

細かい仕様を使ったSchemaの実例を示す。

"id"プロパティがスキーマ要素のIDなのか、対象とするJSONのObject値の中のidプロパティを指すのか、を注意して読もう。また、JSON内にはコメントを書くことができないが、説明上//記号以下はコメントとして扱う。

{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "id": "http://blog.wktk.co.jp/schema/schema.json#",

  "definitions": {
    "name": {
      "id": "#name",
      // http://blog.wktk.co.jp/schema/schema.json#name
      // もしくは、JSON Pointerを使って
      // http://blog.wktk.co.jp/schema/schema.json#/definitions/name
      // で参照できる
      "type": ["string", "null"]
    },
    "email": {
      "id": "#email",
      "type": ["string", "null"]
    },
    "text": {
      "id": "#text",
      "type": "string"
    }
  },
  "type": "object",
  "properties": {
    "entries": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "id": { // これは、$refで参照できるidではない。idという名前のプロパティについてのschema。
            "type": "integer"
          },
          "title": {
            "type": "string"
          },
          "name": {
            "id": "#entry-author",
            "$ref": "#name"
          },
          "email": {
            "$ref": "#email"
          },
          "icon": {
            "type": "string"
          },
          "image": {
            "type": "string"
          },
          "text": {
            "$ref": "#text"
          },
          "comments": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": { // これは、$refで参照できるidではない。idという名前のプロパティについてのschema。
                  "type": "integer"
                },
                "name": {
                  "$ref": "#name"
                  // "$ref": "#/definitions/name" でもいい
                  // "$ref": "#entry-author" でもいい(結局#nameを参照する)
                  // "$ref": "#/properties/entries/items/properties/name"でもいい(結局以下略)
                },
                "email": {
                  "$ref": "#email"
                },
                "text": {
                  "$ref": "#text"
                }
              }
            }
          }
        }
      }
    }
  }
}

JSON Schema Validation

  • Coreに追加する形で、値に対する制約をきつく、もしくはゆるやかにできる拡張
    • title/descriptionなどのメタ情報
    • デフォルト値(default)
    • Objectの必須プロパティ(required)
    • 最大値・最小値
    • 正規表現
    • 複数の値のうちのどれか(enum) ex.) "enum”: [“red”, “blue”, null]
    • 複数のスキーマのAND/OR/NOT(allOf/anyOf/oneOf/not)
    • etc.
  • allOf/anyOf/oneOf/notと$refの組み合わせで、参照したスキーマに手を加えたスキーマを作ることができる
    • これは実例みたほうが分かりやすい

というわけで、JSON Schema Core+ValidationでのSchemaの一例。一部の実装では、formatとか実装されてなかったりする。

{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "id": "http://blog.wktk.co.jp/schema/schema.json#",

  "definitions": {
    "name": {
      "id": "#name",
      "pattern": "^[A-Za-z_]+$", // 正規表現もいける
      "type": ["string", "null"]
    },
    "email": {
      "id": "#email",
      "format": "email", // emailというフォーマットは仕様で定義されている
      "type": ["string", "null"]
    },
    "text": {
      "id": "#text",
      "minLength": 3, // 最低3文字は欲しいよね
      "maxLength": 1000000, // 100万文字まで!
      "type": "string"
    }
  },
  "type": "object",
  "properties": {
    "entries": {
      "type": "array",
      "description": "ブログエントリの一覧だよ",
      "items": {
        "type": "object",
        "description": "ブログエントリだよ",
        "properties": {
          "id": {
            "type": "integer"
          },
          "title": {
            "type": "string"
          },
          "name": {
            "id": "#entry-author",
            "$ref": "#name"
          },
          "email": {
            "$ref": "#email"
          },
          "icon": {
            "type": "string",
            "format": "uri" // format: uriも仕様で定義されている
          },
          "image": {
            "type": "string",
            "format": "uri"
          },
          "text": {
            "$ref": "#text"
          },
          "comments": {
            "type": "array",
            "maxItems": 100, // コメントは1エントリ100個まで
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer"
                },
                "name": {
                  "$ref": "#name"
                },
                "email": {
                  "$ref": "#email"
                },
                "text": {
                  "$ref": "#text"
                }
              }
            }
          }
        }
      }
    }
  }
}

このSchema、id, name, email, textの4つ組はセットで出てくるので、まとめられそうじゃね? というわけで、allOfと$refを使ってまとめてみる。

{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "id": "http://blog.wktk.co.jp/schema/schema.json#",

  "definitions": {
    "text_by_person": {
      "id": "#text_by_person",
      "type": "object",
      "properties": {
        "id": {
          "type": "integer"
        },
        "name": {
          "pattern": "^[A-Za-z_]+$",
          "type": ["string", "null"]
        },
        "email": {
          "format": "email",
          "type": ["string", "null"]
        },
        "text": {
          "minLength": 3,
          "maxLength": 1000000,
          "type": "string"
        }
      }
    }
  },
  "properties": {
    "entries": {
      "type": "array",
      "description": "ブログエントリの一覧だよ",
      "items": {
        "description": "ブログエントリだよ",
        "allOf": [ // 配列内の全ての条件を満たす
          {
            "$ref": "#text_by_person"
          },
          {
            "properties": {
              "title": {
                "type": "string"
              },
              "icon": {
                "type": "string",
                "format": "uri"
              },
              "image": {
                "type": "string",
                "format": "uri"
              },
              "comments": {
                "type": "array",
                "maxItems": 100,
                "items": {
                  "$ref": "#text_by_person"
                }
              }
            }
          }
        }
      }
    }
  }
}

JSON Schema CoreとValidationでできること

CoreとValidationで、JSON SchemaでJSONの構造や値の型・形式などが、Schemaに合致するかを検査することができる。

しかし、JSON Schemaそのものは、そのSchemaが表すリソースのURLや、リソースに対する情報取得・更新などの操作の情報を持たない。よって、APIサーバでのValidationに使いたい場合には、自らエンドポイントごとに、そしてリクエスト・レスポンスごとにJSON Schemaを定義する必要がある。

なお、JSON Schema CoreとValidationを理解するためには、仕様書の前にUnderstanding JSON Schemaを読むといい。

JSON Hyper-Schema

ざっくり、URIごとに、そのURIにひもづくJSON Schemaを指定するもの。記述としては、JSON SchemaにURIをひもづける形式になる。

Schemaに、こんな要素が追加されます。

  • links: SchemaにひもづくURIたちを指定する
    • 具体的には、LDO (Link Description Object)という形式の配列
    • href: Schemaになんらかの形でひもづくURIたち。正確にはURIのテンプレートたち。
    • 例) "/{id}/comments"
    • rel: Schemaが表すインスタンスと、リンク先との関係性を表す
    • 値は自分で決める
    • いくつかの値は、仕様で標準的な用途が決められている
      • self: インスタンス自身のURLを指定する
      • create: 新しいインスタンスを作成するリンクを表す
      • root: インスタンス内へのリンクを表す。hrefはフラグメントだけとなる。
      • etc.
    • title: タイトル
    • targetSchema: URIが返すJSONが従うべきSchema。他のSchemaで定義することもできる値ではある。
    • mediaType: image/pngとかtext/htmlとか。
    • method: GETとかPOSTとか
    • encType: リクエストに付与するパラメータのエンコード形式。application/x-www-form-urlencodedとか。
    • schema: リクエストに付与するパラメータのスキーマ。
  • media: JSONの文字列にエンコードされたデータについて、元の情報を提供
    • binaryEncoding: どんなエンコーディング方式か。base64とか。
    • type: どんな形式か。image/pngとか。
  • readOnly: 値が変更不可であることを示す。
  • pathStart: あるURIから得られたインスタンスについて、複数のスキーマが定義されているときに、URIにpathStartが最長前方一致するスキーマだけが有効となる。

  • 例: ブログのエントリを表すJSONのSchemaの場合。エントリそのもののデータに加えて、以下のような情報を付加できる

    • リンク
    • ブログエントリを表すJSONを返すURI
    • エントリについたコメント一覧を返すURL(GET)
    • エントリについてコメント一覧を検索するURLと、そのURLに渡すパラメータのSchema(パラメータ付きGET)
    • コメントを投稿するURLと、そのURLに渡すパラメータのSchema(POST)
    • エントリの画像(リンクとして)
    • media
    • エントリの画像(base64などで埋め込む場合)

実際、今までのブログのSchemaにこれらの情報を付加してみよう。こんな感じ。

{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "id": "http://blog.wktk.co.jp/schema/schema.json#",

  // Hyper-Schemaのキモであるlinks要素
  "links": [
    // URLにスキーマを結びつける。
    // http://blog.wktk.co.jp/entries で取得できるJSONは、このSchemaに適合しますよ
    {
      "rel": "self", // この用途のrelはselfを指定すると仕様で決められてる
      "href": "/entries"
    },
    // エントリが持つコメント一覧へのリンク
    // 例) http://blog.wktk.co.jp/entries/123/comments
    {
      "rel": "comments", // このrelは適当に定義したもの。
      "href": "/entries/{id}/comments"
    },
    // コメントサーチ用のリンク GET
    // 例) http://blog.wktk.co.jp/entries/123/comments?searchTerm=検索ワード&itemsPerPage=50
    {
      "rel": "comments_search",
      "href": "/entries/{id}/comments",
      "schema": {
        "type": "object",
        "properties": {
          "searchTerm": {
            "type": "string"
          },
          // itemsPerPageは10の倍数で最低10, デフォルト20
          "itemsPerPage": {
            "type": "integer",
            "minimum": 10,
            "multipleOf": 10,
            "default": 20
          }
        },
        // searchTermは必須
        "required": ["searchTerm"]
      }
    },
    // コメントの投稿リンク POST
    // 例) http://blog.wktk.co.jp/entries/123/commentsに以下のようなJSONを投げると、コメントが付与できる
    // {
    //   "message": "とおりすがりの俺がコメント"
    // }
    {
      "rel": "create", // createは、何かインスタンスを作る操作のときに使う
      "title": "Post a comment",
      "href": "/entries/{id}/comments",,
      "method": "POST",
      "schema": {
        "type": "object",
        "properties": {
          "name": {
            "pattern": "^[A-Za-z_]+$",
            "type": ["string", "null"]
          },
          "email": {
            "format": "email",
            "type": ["string", "null"]
          },
          "text": {
            "type": "string"
          }
        },
        // コメントの内容たるtextは必須ですよ
        "required": ["text"]
      }
    },
    // エントリのトップ画像
    {
      "rel": "top_image",
      "href": "/entries/{id}/image",
      // MIME typeを指定できます
      "mediaType": "image/*"
    }
  ],

  "definitions": {
    // 略
  },
  "properties": {
    // 略
            "properties": {
              // 略
              // iconについて、今までURIで指定していたが、BASE64でエンコードしたPNG画像を直接埋め込んでみる
              "icon": {
                "type": "string",
                // mediaはHyper-Schemaで定義されている
                "media": {
                  "binaryEncoding": "base64",
                  "type": "image/png"
                }
              },
              // 略
            }
    // 略
  }
}

JSON Schema全体でできること

URIとSchemaの対応をlinksで記すことによって、JSONを受け取り返却するAPIサーバのリクエスト/レスポンスについて、形式的な情報を記すことができるようになった。

Hyper-Schemaまでの内容を利用して、以下のようなことができるミドルウェアがいくつかある。

APIサーバでのバリデーション

  • JSON Hyper-Schemaで定義されたURIに対するrequest/responseが指定のSchemaに合致するJSONかどうかチェックする

APIサーバのスタブ生成

  • Schemaに準じたランダムの値を返すAPI Webサーバを立てる

APIドキュメントの生成

  • URIのエンドポイントごとに、どのようなリクエストのJSONを投げたら、どのようなレスポンスを得られるかのドキュメント
  • 実際にリクエストを投げるフォームを持ったHTMLドキュメントの生成

APIクライアントの生成

  • URIのエンドポイントごとに、必要なパラメータを渡したら、レスポンスが返ってくるような関数の生成

ミドルウェアの例

ミドルウェアの例はこれら。

JSON Schemaのデメリット

SchemaのJSONを書くのが煩雑。

  • 対策1: http://jsonschema.nethttps://github.com/kenchan/schemize でJSONの実例からスキーマのひな形を作ることができる
  • 対策2: 複数のJSON Schemaをマージするツールがある。prmdなど。
  • 対策3: YAMLのほうがJSONより書くのが楽ならば、いくつかのツールではYAMLを使ってJSON Schemaを出力することができる。prmdなど。

JSON Schemaまとめ

  • JSON Schemaは以下の3つの仕様の総体
    • JSON Schema Core: 型と構造
    • JSON Schema Validation: より詳細な値の形式と構造
    • JSON Hyper-Schema: URIにひもづいたJSON Schema
  • できること
    • APIサーバのバリデーション
    • APIサーバのスタブ生成
    • APIドキュメントの生成
    • APIクライアントの生成
  • デメリット
    • 書くのがめんどい

次回予告

次回は、JSON API v.1について書く予定だよ。HALとかとの比較も載せられる…といいな。