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というプロパティ
- $refでの参照する際に指定する。
- IDの値として、URIを指定する。 ex.) “id”: “http://example.com/schemas/product.json#”
- スコープの中のid/$refの値に対するURIのベースを指定する。HTMLのbaseタグみたいな感じ。
- ex.) 祖先の要素でidが http://example.com/schemas/product.json だったら、"$ref": “person.json"は http://example.com/schemas/person.json を指す
- ex.) 祖先の要素でidが http://example.com/schemas/product.json だったら、"id”: “path/address.json#zipcode"は http://example.com/schemas/path/address.json#zipcode を指す
- 大抵の場合は、JSON SchemaのルートObjectのidプロパティに#で終わるURLを指定し、それ以外のObjectのidプロパティには#で始まるSchema内ローカルなidを付与する。
- このようにしておくと、”http://example.com/schema/schema.json#document_local_id“ 的なURIでスキーマの要素がアクセスできる。
- 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.net や https://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とかとの比較も載せられる…といいな。