このブログの更新は Twitterアカウント @m_hiyama で通知されます。
Follow @m_hiyama

メールでのご連絡は hiyama{at}chimaira{dot}org まで。

はじめてのメールはスパムと判定されることがあります。最初は、信頼されているドメインから差し障りのない文面を送っていただけると、スパムと判定されにくいと思います。

参照用 記事

最近のJSONスキーマを解説します

現在、JSONスキーマIETFのインターネットドラフトとなっています。

僕がはじめてJSONスキーマを紹介した時(2009年4月)と比べて、一番大きな変化はハイパースキーマ(hyper schema)と呼ばれる仕様が加わったことです。ハイパースキーマは、JSONデータをハイパーメディアと見なすための仕様です。個人的には欲しかった機能ですが、ちょっと「こりゃダメかも感」がただよっています。ハイパースキーマはいずれ話題にすることがあるかも知れませんが、今日の話は従来からあったコアスキーマ仕様に限定します。

この記事では、現状のJSONアスキーマ仕様に基づき、スキーマの書き方を解説します。なお、我々のCatyスキーマ記述言語のこともしばしば「JSONスキーマ」と呼んでいますが、この記事で単に「JSONスキーマ」と書いたら本家JSONスキーマのことで、Catyスキーマ記述言語は「Catyスキーマ」と書きます。

内容:

型表現がJSONインスタンス

型を表す式を型表現(type expression)と呼びます。JSONスキーマの特徴は、型表現がまたJSONデータであることです。これには、メタデータをデータとして扱えるという大きな利点があります。しかし、人間が読み書きするには辛いものがあります。Catyスキーマ記述言語では、読み書きのしやすさを最優先しました。マイクロフォーマットの構造的人名JSONスキーマとCatyスキーマの型表現*1で書いてみます。

JSONスキーマ

{
 "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種類あります。

  1. 文字列を使った単純な型表現: "string", "object" など。
  2. 配列を使った型表現: ["string", "integer"] など。
  3. オブジェクトを使った型表現: {"type" : "array", "items" : {"type" : "integer"}} など。

文字列は定義済み型の名前です。"integer", "number", "string", "boolean", "object", "array", "null", "any" が使えます。配列形式はユニオン型を表します。オブジェクト形式は、より複雑な型表現のために使います。

型表現には冗長性があって、次の3つの表現は同じ意味です。

  1. "string"
  2. ["string"]
  3. {"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スキーマではもう少し細かく分類します。

  1. 基本スカラー型: integer, number, string, boolean, null
  2. リスト型: 項目がすべて同じ型である配列型、項目の個数は任意
  3. タプル型: 決まった個数の項目を持つ配列型
  4. タプル+リスト型: 決まった個数の項目と、それに続く同じ型の任意個の項目を持つ配列型
  5. 閉じたオブジェクト型: 決まった個数のプロパティを持つオブジェクト型
  6. 開いたオブジェクト型: 決まった個数のプロパティと、同じ型の任意個のプロパティを持つオブジェクト型
  7. ユニオン型: 複数の型のどれかを意味する型
  8. 列挙型: 有限個の定数リテラルのどれかを意味する型
  9. 全称型: 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つで全部です。

  1. integer
  2. number
  3. string
  4. boolean
  5. 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に導入しようとしているからです。しかし、残念ながらハイパースキーマ仕様は出来が悪い; このままではどうも無理がありそうです。