檜山正幸のキマイラ飼育記 このページをアンテナに追加 RSSフィード Twitter

キマイラ・サイトは http://www.chimaira.org/です。
トラックバック/コメントは日付を気にせずにどうぞ。
連絡は hiyama{at}chimaira{dot}org へ。
蒸し返し歓迎!
このブログの更新は、Twitterアカウント @m_hiyama で通知されます。
Follow @m_hiyama
ところで、アーカイブってけっこう便利ですよ。

2009-04-13 (月)

JSONの可能性がグンと拡がるぞ! JSONスキーマ

| 09:14 | JSONの可能性がグンと拡がるぞ! JSONスキーマを含むブックマーク

JSON(http://www.json.org/)データはけっこうよく使うので、何度か話題にしたことがあります(例えば「もう一度、ちゃんとJSON入門」)。でも、JSONには型情報/メタ情報が付けられないのがとても不満で、JSON改なんてもんを考えたこともありました。(後でXIONに改名)

JSONデータに対するスキーマ定義の仕様がかたまりつつあることを、ごく最近になって知りました。

JSON本体はRFC 4627になっていますが、JSONスキーマの標準化のステータスは、あまりハッキリとは分かりません(僕には)。http://groups.google.com/group/json-schema?pli=1 を覗き見した感じでは、現状ワーキングドラフトという位置付けらしいです。

なかなか面白いし役に立ちそうなので紹介します。ただし、僕にとって興味があって必要な部分だけを解説します。言い残した事項を最後に指摘することにします。

内容:

  1. まずはサンプル
  2. JSONバリデータ
  3. 一般的な型(組み込み型)
  4. スキーマの書き方、その第一歩
  5. スキーマ属性による制約
  6. よく使いそうなスキーマ属性
  7. オブジェクト型と配列型のスキーマ定義
  8. 言い残したこと

●まずはサンプル

かつてJSON改のサンプルとして次のようなデータ・インスタンスを出しました。これはプレーンなJSONデータではなくて、型情報を付加したものです。

// 平面内の折れ線
polygonalLine [
  point{"x":0, "y":0},
  point{"x":1, "y":0},
  point{"x":1, "y":2},
  point{"x":0, "y":3}
]

// 人に関する情報
person {
  "name" : "米倉花子",
  "age"  : age 23,
  "mailAddress" : mail"hanako-y@hoge.example.jp",
  "otherContacts" : [tel"03-9999-0000"]
}

// <greeting mode="friendly">Hello<smil/></greeting> 
// の翻訳
greeting {
 "mode" : "friendly",
 "" : ["Hello", smile{}]
}

型情報を取り除いてしまうと次のようになります。単なるJSONデータですね。型情報がないと意味不明になっちゃう部分もあります。

// 平面内の折れ線
[
  {"x":0, "y":0},
  {"x":1, "y":0},
  {"x":1, "y":2},
  {"x":0, "y":3}
]

// 人に関する情報
{
  "name" : "米倉花子",
  "age"  : 23,
  "mailAddress" : "hanako-y@hoge.example.jp",
  "otherContacts" : ["03-9999-0000"]
}

// <greeting mode="friendly">Hello<smil/></greeting> 
// の翻訳
{
 "mode" : "friendly",
 "" : ["Hello", {}]
}

それぞれの型をJSONスキーマで記述するなら、例えば次のようになります。とりあえず眺めてみてください。

// 平面内の折れ線
{
  "description": "平面内の折れ線",
  "type" : "array",
  "items" : {
     "description" : "平面内の点",
     "type" : "object",
     "properties" : {
        "x" : {"type" : "integer"},
        "y" : {"type" : "integer"}
     }
  }
}

// 人に関する情報
{
  "description": "人に関する情報",
  "type" : "object",
  "properties" : {
     "name" : "string",
     "age" : "integer",
     "mailAddress" : "string",
     "otherContacts" : {
        "type" : "array",
        "items" : {
           "type" : "string"
        }
     }
  }
}

// <greeting mode="friendly">Hello<smil/></greeting> 
// の翻訳
// ただし、属性modeはオプショナル、内容モデルは任意とする
{
  "description" : "greeting",
  "type" : "object",
  "properties" : {
     "mode" : {"type" : "string", "optional" : true},
     "" : {
        "type" : "array",
        "items" : {
           "type" : "any"
        }
     }
  }
}

あんまり分かりやすくないですよね。だからこれから説明します。が、先にバリデータ実装を紹介しておきます。

●JSONバリデータ

JSONデータ・インスタンスが、スキーマに対して妥当(valid)かどうかを検証するツールがバリデータです。http://code.google.com/p/jsonschema/ から、JavaScriptとPythonによるバリデータが入手可能です。僕はJavaScript版jsonschema-b2.js(http://jsonschema.googlecode.com/files/jsonschema-b2.js)をRhinoで動かしてみました。

js> JSONSchema.validate(someJsonData, mySchema)

のようにして使います。

スキーマ(これもJSONデータ)の構文的正しさを調べるには、The JSON Schema for JSON Schemas(http://jsonschema.googlecode.com/files/schema)を使って、次のようにすればいいでしょう*1

js> var S4S = {"description":"This is the JSON Schema for JSON Schemas.",
 "type":["object","array"],
 // 省略
}
js> var result = JSONSchema.validate(mySchema, S4S)

resultを表示するために、次のような関数を準備しました。

function show(result) {
 if (!result.valid) {
  var e = result.errors;
  for (var i = 0; i < e.length; i++) {
    print ("(" + (i+1) + ") '" + e[i].property + "' " + 
           e[i].message + ".");
  }
 } else {
  print("valid.")
 }
}

resultはオブジェクトで、2つのプロパティを持ち …… いやいや、ここはやっぱりresultに対するJSONスキーマを提示すべきでしょう。

{
  "description" : "バリデーションの結果",
  "type" : "object",
  "properties" : {
     "valid" : {"type" : "boolean"},
     "errors" : {
        "type" : "array",
        "items" : {
           "type" : "object",
           "properties" : {
              "property" : {"type" : "string"},
              "message" : {"type" : "string"}
           }
        }
     }
  }
}

以下において、キーボード操作を簡単にするために、次の関数(事実上エイリアス)も定義しておきます。

function valid(data, schema) {
 show(JSONSchema.validate(data, schema));
}

実際に動かしてみると、jsonschema-b2.jsはときに不可解な挙動を示します(後述)。仕様のほうを調べても明確な記述はありません。安定した仕様と実装が手に入るには、もう少し時間がかかりそうです。

●一般的な型(組み込み型)

JSONスキーマで最初から使える型は次のものです。JSONスキーマ仕様では、simple typesと呼んでますが、通常の感覚ともXML Schemaなどの用語法とも相容れないので、組み込みの一般的型と呼んでおきましょう。

  • 基本(スカラー)型
    • string -- 文字列、デフォルトのエンコーディングはUTF-8*2
    • number -- 整数または浮動小数点数
    • integer -- 整数、ただしビット数や範囲は規定しない
    • boolean -- trueとfalse
  • 複合型
    • object -- キー/値ペアの集合
    • array -- 配列、要素の型が一定である必要はない
  • 特殊な型
    • null -- 値nullのみからなる型
    • any -- 任意の型

integerはnumberの部分集合になっています。浮動小数点だけを表す型はありませんが、実用上不便はないでしょう。バリデータの挙動からは、arrayはobjectの部分集合型のようですが、明確な記述は発見できませんでした*3。その他の型(の値集合)のあいだに包含関係はありません。

JSONスキーマの型は、もとのJSON仕様の型*4、JavaScriptの型と多少食い違っています。

JSONスキーマ JSON JavaScript
string string string
number number number
integer - -
boolean - boolean
object object object
array array -
null - -
any - -

また、複合型の成分に関する用語法も、もとのJSON仕様から変更されています。

JSONスキーマ JSON JavaScript
配列の成分 項目(item) 要素(element) 要素(element)
オブジェクトの成分 プロパティ(property) メンバ(member) プロパティ(property)

いま述べた組み込み型に対して制約を加えたりユニオンを作ることにより、目的にあった独自の型を定義します。その定義を行う構文とメカニズムがJSONスキーマというわけです。

スキーマの書き方、その第一歩

JSONスキーマは、それ自体がJSONオブジェクト(object型データ)になっています。スキーマとして最も基本的なプロパティはtypeです。

{
  "description" : "an array",
  "type" : "array"
}

このスキーマは、目的のデータが配列データであることを意味します。descriptionはコメントのようなもので必須ではありません(が、書いておくことを推奨します)。

実際のバリデータで試してみましょう。

js> valid(3, {"type":"array"})
(1) '' number value found, but a array is required.
js> valid("", {"type":"array"})
(1) '' string value found, but a array is required.
js> valid(true, {"type":"array"})
(1) '' boolean value found, but a array is required.
js> valid({}, {"type":"array"})
(1) '' object value found, but a array is required.
js> valid([], {"type":"array"})
valid.
js> 

配列以外のデータインスタンスは不正扱いしてますから、ちゃんとバリデートしています。

js> valid(null, {"type":"array"})
js: "jsonschema-b2.js", line 177: uncaught JavaScript runtime exception:
 TypeError: Cannot read property "$schema" from null
js> valid(undefined, {"type":"array"})
js: "jsonschema-b2.js", line 177: uncaught JavaScript runtime exception:
 TypeError: Cannot read property "$schema" from undefined
js> 

ウーン、jsonschema-b2.jsは、いきなり"$schema"というプロパティを見にいって(埋め込まれたスキーマ定義を探しにいって)ランタイムエラーしているみたい。

js> valid(null, {"type": "null"})
js: "jsonschema-b2.js", line 177: uncaught JavaScript runtime exception:
 TypeError: Cannot read property "$schema" from null
js> 

アリャリャ、null型の定義からすると、valid(null, {"type": "null"})は妥当なはずですが。

js> valid("", {"type":"integer"})
valid.
js> valid(true, {"type":"integer"})
valid.
js> valid({}, {"type":"integer"})
valid.
js> valid([], {"type":"integer"})
valid.
js> 

ムーー。こりゃバグでしょう。次が期待される挙動だと思います。(画面のコピーじゃなくて、僕が手で書いたものです。)

js> valid("", {"type":"integer"})
(1) '' string value found, but an integer is required.
js> valid(true, {"type":"integer"})
(1) '' boolean value found, but an integer is required.
js> valid({}, {"type":"integer"})
(1) '' object value found, but an integer is required.
js> valid([], {"type":"integer"})
(1) '' array value found, but an integer is required.
js> 

なんか処理系に文句付ける話になっちゃったけど、{"type" : "<型名>"} って書き方はいいですよね、これで。

スキーマ属性による制約

「0以上の整数」(非負の整数)というデータ型を定義するには次のようにします。

{
  "type" : "integer",
  "minimum" : 0
}

js> valid(3, {"type" : "integer", "minimum" : 0})
valid.
js> valid(-1, {"type" : "integer", "minimum" : 0})
(1) '' must have a minimum value of 0.

1以上10以下の整数なら次のようにします。

{
  "type" : "integer",
  "minimum" : 1,
  "maximum" : 10,
}

js>  valid(0, {"type" : "integer", "minimum" : 1, "maximum": 10})
(1) '' must have a minimum value of 1.
js> valid(1, {"type" : "integer", "minimum" : 1, "maximum": 10})
valid.
js> valid(5, {"type" : "integer", "minimum" : 1, "maximum": 10})
valid.
js> valid(10, {"type" : "integer", "minimum" : 1, "maximum": 10})
valid.
js> valid(11, {"type" : "integer", "minimum" : 1, "maximum": 10})
(1) '' must have a maximum value of 10.
js> 

今出てきたminimumやmaximumのように、既存の型を制限するための指定項目をスキーマ属性といいます。XML Schema Part 2: Datatypes では、ファセット(facet)と呼んでいた機構と同じです。スキーマ属性は、値の集合上の述語(predicate)を簡略に記述する手段を与えます。

●よく使いそうなスキーマ属性

僕の観点からよく使いそうだと思えるスキーマ属性を挙げます。全部ではありません。

integer, numberに使えるスキーマ属性

  • minimum -- 最小値
  • maximum -- 最大値

stringに使えるスキーマ属性

  • minLength -- 文字列の最小の長さ
  • maxLength -- 文字列の最大の長さ

ただし、長さが何を意味するか難しい問題だと思います。長さ=文字数だとすると、「文字とは何か」という哲学的にも実務的にも困難な問に答えなくてはなりません*5

integer, number, stringに使えるスキーマ属性

  • enum -- 指定された有限個の値だけが許される

配列に使える属性

  • minItems -- 配列の最小の長さ
  • maxItems -- 配列の最大の長さ

プロパティの出現性に関するスキーマ属性

  • optional -- trueならそのプロパティオは省略可能。デフォルトはfalse
  • default -- そのプロパティが出現しなかったときのデフォルト値

●オブジェクト型と配列型のスキーマ定義

JSONスキーマが見づらい理由のひとつは、type属性が他のスキーマ属性と同列に書かれていることでしょう。

{
  "type" : "integer",
  "minimum" : 1,
  "maximum" : 10,
}

ここで仮に、type属性だけを特別扱いしてみます。また、鬱陶しい引用符も省略します。すると次のように書けます。

 integer {minimum : 1, maximum : 10}

オブジェクトに関しては次のように書いたら見やすいと思います。

object {} {
  "name" : string,
  "age" : integer {minimum : 0, maximum : 150},
  "mailAddress" : string
}

objectの直後に{}が入っているのは、object型を制約するスキーマ属性が入る場所としてです。これを書いておかないと、スキーマ属性とプロパティの型定義が構文的に区別できなくなります*6

配列を、番号をキーにしたオブジェクト(連想配列)と考えれば、「最初がinteger、次がstringである配列」の型定義は次のように書いてもいいでしょう。

array {} {
 0 : integer,
 1 : string
}

すべての配列要素が同じ型なら、ワイルドカード*を使って次のように書けます。

array {} {
 * : integer
}

以上の記法により、次の条件を満たす「折れ線の型」を定義してみましょう;「長さ(頂点の数 - 1)が1以上3以下で、すべての頂点が 0≦x≦100, 0≦y≦200の矩形内に入るような折れ線」:

array {minItems: 2, maxItems: 4} {
 * : object {} {
       "x" : integer {minimum: 0, maximum: 100},
       "y" : integer {minimum: 0, maximum: 200}
     }
}

実際のJSONスキーマだと、次のようになります。

{
  "type" : "array",
  "minItems" : 2, 
  "maxItems" : 4,
  "items" : {
     "type" : "object",
     "properties" : {
        "x" : {"type" : "integer", "minimum" : 0, "maximum": 100},
        "y" : {"type" : "integer", "minimum" : 0, "maximum": 200}
     }
  }
}

オブジェクトと配列の型記述に関して、次のパターンに慣れてしまえばいいのです。

 "type" : "object",
 "properties" : {
    "<キー名>" : <型定義>,
    ……
    ……
 }

 "type" : "array",
 "items" : <型定義>

配列を長さnのタプルとして使用する場合の型定義は次のようです。

 "type" : "array",
 "items" : [
    <型定義1>,
    ……
    <型定義n>
 ]

●言い残したこと

「JSONスキーマをJSON構文で書く」メリットは理解できるのですが、読みにくい/書きにくいのは相当に痛い欠点だと思います。ローカル/インターナルに使用するなら、もう少し簡略な構文でもいいんじゃないかな、とか考えてます。実験もしてみたので、そのうち書くかも。

*1:propertiesの綴りを間違えてporpertiesとなっていても指摘はしないで、valid(合っている)と言ったりします。現状では、バグか仕様かハッキリしません。

*2:文字列のエンコーディングというよりは、JSONデータ全体のデフォルトエンコーディングがUTF-8。RFC4627のセクション3. "Encoding" に、JSON text SHALL be encoded in Unicode. The default encoding is UTF-8. と書いてあります。

*3:JavaScriptでは、配列はオブジェクトの一種ですが、一般的には配列とオブジェクトは区別すべきだと思います。

*4:JSON仕様では正式に型を導入していません。ここでの型は、構文定義から読み取れる“型らしき種別”を指します。

*5:実際上は、エンコードされた表現のなかに含まれるUnicodeコードポイントの個数って定義になるでしょう。しかし、コードポイントが完全な文字を表していないときもあります。文字素片、合成文字がありますから。

*6:改行に意味を持たせて、次の行からプロパティの型定義をはじめる、とかにすれば{}のようは手段は不要になります。

*7:もともと仕様にあった例では、http://mydomain.com/ が使われていたけど、mydomain.comって実在するドメイン屋さんなんだよね。