現在、JSONスキーマはIETFのインターネットドラフトとなっています。
僕がはじめてJSONスキーマを紹介した時(2009年4月)と比べて、一番大きな変化はハイパースキーマ(hyper schema)と呼ばれる仕様が加わったことです。ハイパースキーマは、JSONデータをハイパーメディアと見なすための仕様です。個人的には欲しかった機能ですが、ちょっと「こりゃダメかも感」がただよっています。ハイパースキーマはいずれ話題にすることがあるかも知れませんが、今日の話は従来からあったコアスキーマ仕様に限定します。
この記事では、現状のJSONコアスキーマ仕様に基づき、スキーマの書き方を解説します。なお、我々のCatyスキーマ記述言語のこともしばしば「JSONスキーマ」と呼んでいますが、この記事で単に「JSONスキーマ」と書いたら本家JSONスキーマのことで、Catyスキーマ記述言語は「Catyスキーマ」と書きます。
内容:
型表現がJSONインスタンス
型を表す式を型表現(type expression)と呼びます。JSONスキーマの特徴は、型表現がまたJSONデータであることです。これには、メタデータをデータとして扱えるという大きな利点があります。しかし、人間が読み書きするには辛いものがあります。Catyスキーマ記述言語では、読み書きのしやすさを最優先しました。マイクロフォーマットの構造的人名をJSONスキーマとCatyスキーマの型表現*1で書いてみます。
{ "type" : "object", "properties" : { "n" : { "type": "object", "properties" : { "family-name" : {"type": "string"}, "given-name" : {"type": "string"} } } } }
Catyスキーマ:
{ "n" : { "family-name" : string, "given-name" : string } }
短く書ける点ではCatyスキーマが上です。が、Catyスキーマの型表現自体はJSONデータではありません。JSONデータとすごく似てますが、実は違います。よって、JSONデータを扱うツールでCatyスキーマを扱うことはできません。例えば、JSONデータをコンテキストとするテンプレートエンジン*2への入力としてCatyスキーマは使えません。JSONスキーマならそれができます。
と、少しだけCatyスキーマの紹介を入れたりしましたが、以下はすべて(本家の)JSONスキーマの話です*3。
型表現とスキーマ
JSONスキーマでは、型表現とスキーマは同義語ではありません。型表現は3種類あります。
- 文字列を使った単純な型表現: "string", "object" など。
- 配列を使った型表現: ["string", "integer"] など。
- オブジェクトを使った型表現: {"type" : "array", "items" : {"type" : "integer"}} など。
文字列は定義済み型の名前です。"integer", "number", "string", "boolean", "object", "array", "null", "any" が使えます。配列形式はユニオン型を表します。オブジェクト形式は、より複雑な型表現のために使います。
型表現には冗長性があって、次の3つの表現は同じ意味です。
- "string"
- ["string"]
- {"type": "string"}
冗長性を減らすために次のルールがあるようです。
- "string" は {"type" : "string"} にする。
- ["string", "integer"] も {"type" : ["string", "integer"]} にする。
- 配列は、項目が2つ以上のときに限って使う。
ただし、このルールをどんどん適用すると、次のようなことになってしまいます。
{ "type" : [ {"type" : {"type" : "string"}}, {"type" : {"type" : {"type" : "integer"}}} ] }
そこで、次のルールもあります。
- {"type" : {"type" : "string"}} のように "type" が重なるのは許さない。
このようなアドホックなルールがうまく機能するかと言うと、残念ながらうまくいきません。冗長性(たくさんの表現が同じ意味を持つ)はあんまり減りません。正規形と正規化の手順が定義されていればいいのですが、見当たりません。その他、細かく見ていくと、変なところ/困ったところがあるのですが、今日は悪口言うのをやめておきます。
さて、JSONデータとして表した型表現のなかで、オブジェクトの形のものを特にスキーマと呼びます。以下に箇条書きにまとめておきますから、次のことはよーく頭に入れおいてください。
- 型表現は、単純文字列("string"など)、配列形式(["string", "integer"]など)、オブジェクト形式の3種がある。
- オブジェクト形式の型表現をスキーマと呼ぶ。
- 文字列と配列による型表現は、{"type" : *} によりラップしてオブジェクト形式にできるので、事実上すべての型表現をスキーマとみなせる。
オブジェクト形式でない型表現は、式(expression)の構成要素として絶対に必要ですが、それをスキーマとは呼ばない点が要注意です。
型の分類とスキーマの基本
JSONの型は、基本スカラー型、配列型、オブジェクト型に分類するのが普通ですが、JSONスキーマではもう少し細かく分類します。
- 基本スカラー型: integer, number, string, boolean, null
- リスト型: 項目がすべて同じ型である配列型、項目の個数は任意
- タプル型: 決まった個数の項目を持つ配列型
- タプル+リスト型: 決まった個数の項目と、それに続く同じ型の任意個の項目を持つ配列型
- 閉じたオブジェクト型: 決まった個数のプロパティを持つオブジェクト型
- 開いたオブジェクト型: 決まった個数のプロパティと、同じ型の任意個のプロパティを持つオブジェクト型
- ユニオン型: 複数の型のどれかを意味する型
- 列挙型: 有限個の定数リテラルのどれかを意味する型
- 全称型: any
これらそれぞれの種類ごとにスキーマの書き方が決まっています。スキーマはそれ自身JSONオブジェクトなので、いくつかのプロパティを持つのですが、次に挙げるプロパティが特に重要です。(以下で、「インスタンス・プロパティ」とは、スキーマ自身のプロパティではなくて、スキーマにより定義される型に所属するインスタンス・オブジェクトのプロパティのことです、十分ご注意を。)
- type : このプロパティの値には、基本となる型表現を指定する。
- properties : typeの値が"object"のとき、そのインスタンス・パロパティ達の型表現を指定する。
- items : typeの値が"array"のとき、そのインスタンス項目達の型表現を指定する。
- additionalProperties : properties, itemsに記述しなかった残りのインスタンス・プロパティまたはインスタンス項目の型表現を指定する。
注意しなくてはいけない事がいくつかあります。まず、typeプロパティが必須ではないことです。typeが省略されれば、{"type": "any"} と同じです。このため、空なオブジェクト {} も合法なスキーマです。それだけでなく、他のプロパティからtypeの値が推測できるときもtypeを省略できます。例えば、propertiesプロパティがあれば "type" : "object" は不要とか、maxLengthプロパティがあれば "type" : "string" は不要とか。この仕様は、処理プログラムを作る立場からはいいかげん迷惑ですが、そうなっているのでしょうがありません。
僕は最初、typeという名のプロパティと、例えばminimum(数の範囲の最小値)という名のプロパティが、並列・同等に扱われるのにすごく違和感があったのですが、typeの意味は「基本とするおおよその制限」という程度です。typeだけで全ての制約を記述するとは限らないし、typeがなくても十分な制約が書けることもあるのです。例えば、{"minimum" : 0} と書くだけでも「0以上の数」という意味を持ちます。
JSONスキーマ仕様で定義されてないプロパティをスキーマに入れてもエラーにはなりません。単に無視されるだけです。これによって例えば、ユーザーインターフェースで使うラベルとか独自のアノテーションなどを付加することもできます。
additionalPropertiesというスキーマ・プロパティがありますが、additionalItemsというスキーマ・プロパティはありません。additionalPropertiesを、additionalItemsの用途にも流用します(僕はこういう流用は嫌いだが)。
JSONスキーマの記述パターン
型の分類ごとに、どのようにスキーマを書くかを説明します。
基本スカラー型
これは簡単です。
{ "type" : "<型の名前>" }
例えば、{"type" : "integer"} とか。基本スカラー型の名前は次の5つで全部です。
- integer
- number
- string
- boolean
- null
リスト型
リストの項目の型を表現するスキーマを、<項目のスキーマ>と書くとします。
{ "type" : "array", "items" : <項目のスキーマ> }
itemsプロパティの値がスキーマなのでオブジェクトである点に注意してください。文字列は書けません。配列を書くと別な意味になります。
タプル型
タプルの項目ごとの型を表現するスキーマを、<項目のスキーマ1>、<項目のスキーマ2>などと書くとします。
{ "type" : "array", "items" : [<項目のスキーマ1>, <項目のスキーマ2>, ..., <項目のスキーマn>], "additionalProperties" : false }
タプル+リスト型
タプルの項目の型を表現するスキーマを、<項目のスキーマ1>、<項目のスキーマ2>など、残りの項目の型は<残余項目のスキーマ>と書くとします。
{ "type" : "array", "items" : [<項目のスキーマ1>, <項目のスキーマ2>, ..., <項目のスキーマn>], "additionalProperties" : <残余項目のスキーマ> }
閉じたオブジェクト型
オブジェクトのプロパティ名を<プロパティ名1>、<プロパティ名2>など、プロパティ値の型を表現するスキーマを、<プロパティのスキーマ1>、<プロパティのスキーマ2>などと書くとします。
{ "type" : "object", "properties" : { "プロパティ名1" : <プロパティのスキーマ1>, "プロパティ名2" : <プロパティのスキーマ2>, ... "プロパティ名n" : <プロパティのスキーマn>, }, "additionalProperties" : false }
開いたオブジェクト型
オブジェクトのプロパティ名を<プロパティ名1>、<プロパティ名2>など、プロパティ値の型を表現するスキーマを、<プロパティのスキーマ1>、<プロパティのスキーマ2>など、残りのプロパティの型は<残余プロパティのスキーマ>と書くとします。
{ "type" : "object", "properties" : { "プロパティ名1" : <プロパティのスキーマ1>, "プロパティ名2" : <プロパティのスキーマ2>, ... "プロパティ名n" : <プロパティのスキーマn>, }, "additionalProperties" : <残余プロパティのスキーマ> }
ユニオン型
ユニオン型は配列で表しますが、構成要素の型表現に、スキーマ(オブジェクト形式)だけでなく文字列による単純な型名も使えます。構成要素の型表現を、<型名またはスキーマ1>、<型名またはスキーマ2>などとします。
{ "type" : [<型名またはスキーマ1>, <型名またはスキーマ2>, ..., <型名またはスキーマn>] }
列挙型
列挙型にはtypeプロパティを使いません*4。
{ "enum" : [<リテラル1>, <リテラル2>, ..., <リテラルn>] }
全称型
{"type" : "any"} または空なオブジェクト {} です。
その他のスキーマ属性
スキーマ、つまりオブジェクト形式の型表現に現れるプロパティはスキーマ属性と呼ばれます。type, propertiesなどもスキーマ属性ですが、これらは型表現の中核を構成するものです。その他に補助的な属性がいくつかあります。
optional属性は、オブジェクトのプロパティの出現性を表します。次が使用例です。
{ "type": "object", "properties" : { "family-name" : {"type": "string"}, "given-name" : {"type": "string", "optional" : true} } }
requiredという属性もありますが、これは "optional" : false とは関係ない別な意味を持ちます(たいしたもんじゃないので説明は省略)。optional属性を、配列型の項目スキーマ内で使えてもいいと思うのですが、JSONスキーマ仕様ではオブジェクト型のプロパティのスキーマでしか使えないことになっています。なんでや?
さらに、特定の型ごとの属性(stringに対するmaxLengthとか、numberに関するminimumとか)がありますが、その意味は単純なので、原仕様書を参照してください。
HTMLとJSON
JSON推進派の人々は、JSONをインターネット上の標準的なデータ交換フォーマットにしたいようです(それは当然ですね)。そればかりではなく、ブラウザなどのユーザーエージェントが、直接的にJSONデータを扱えるようにすることも目論んでいる気配です。現在、HTMLが“フォーマットの王”ですが、JSONがその王座を狙う …… というのは無理でも、二番手の地位を確立したい、ということでしょう*5。
僕自身は、HTMLとJSONの違いは認識しながらも、「HTMLとJSONのどちらでもいいよ -- 構造的には等価だよ」という用途や分野に注目しているので(「プログラマ/デザイナの境界としてのクリーンHTML」参照)、「JSONをHTMLのように使う」あるいは「HTMLをJSONのように使う」方向性は歓迎です。そのとき、JSONの優位性のひとつは、軽量なスキーマにより型定義ができることでしょう。
*1:以前の仕様ではなくて、undocumentedな最新の仕様です。
*2:Catyのテンプレートエンジンがまさにその例です。
*3:CatyスキーマとJSONスキーマは、構文は大幅に違いますが、セマンティクスはほぼ同じです。
*4:{"type" : "integer", "enum" : [1, 2, 3]} のように書いてもエラーではありません。
*5:そう思える根拠は、新しく追加されたJSONハイパースキーマ仕様により、ハイパーリンクやHTMLフォームのような機能をJSONに導入しようとしているからです。しかし、残念ながらハイパースキーマ仕様は出来が悪い; このままではどうも無理がありそうです。