JSONスキーマについて書いた2つのエントリー「JSONの可能性がグンと拡がるぞ! JSONスキーマ」と「JSONスキーマの功罪を、印象や感情じゃなくて考えてみようか」において、僕は次のように書きました。
読みにくい/書きにくいのは相当に痛い欠点だと思います。ローカル/インターナルに使用するなら、もう少し簡略な構文でもいいんじゃないかな、とか考えてます。
直接手で書いたり目で見たりすることを考えると、あの構文はどうにもいただけません。JSONオブジェクトにスキーマを埋め込む(自己記述的なデータを作る)ためには、JSONスキーマはJSON構文で書かなくてはならないのですが、インスタンスとスキーマが分離した状況では、(原理的には)スキーマに別構文を採用するのも許されるはずです。あの構文は、僕の感情的あるいは生理的な許容範囲を超えているので、たぶん意味的には同値な別構文をでっち上げるでしょうね、ローカル/インターナルな使用では。
そんな事情で、JSONスキーマとほぼ同等でもっと読みやすい構文をいくつか考えました。そのうちの1つについて記します。同じ問題意識で書かれたyojikさんの記事がとても面白いし、僕もこの記事に示唆を受けています。
内容:
単純なパターンによる型記述
とりあえず安直に考えて、JSONのインスタンスと同じ形式で、値の代わりに型を記述したパターンが簡単そうです。
人に関する情報のスキーマなら次のようになります。
{ "name" : string, "age" : integer, "mailAddress" : string }
見やすくていいですよね。
配列型のパターン記述としては、[integer]
と書くと「整数の配列(リスト)」を意味する構文をよく見かけます。しかしこれだと、「最初の項目が整数、2番目(インデックスは1)の項目が文字列である配列」を表現できません。[integer, string]
で「最初が整数、2番目が文字列」を表すなら自然です。
ところがこうすると、「項目の型が整数で、長さは任意である配列」がうまく表現できません。正規表現の星印「*」を借りてくることにしましょう。
[integer*]
-- 項目の型が整数で、長さは任意である配列[integer, integer*]
-- 項目の型が整数で、長さ1以上の配列[integer, string, any*]
-- 最初が整数、2番目が文字列、その後は任意である配列
これを使うと、次のような記述ができます。
{ "name" : string, "age" : integer, "mailAddress" : string, "otherContacts" : [string*] }
オブジェクトの追加プロパティの問題
さて、すぐ上に出したスキーマに対して、次のインスタンスは妥当でしょうか?
{ "name" : "米倉花子", "age" : 23, "mailAddress" : "hanako-y@hoge.example.jp", "otherContacts" : ["03-9999-0000"], "hobby" : "買い物" }
hobbyというプロパティが余分ですが、これを認めてよいのでしょうか? JSONスキーマ仕様では、additionalProperties というスキーマ属性があり、明示的に指定してないプロパティに何を認めるかを書けるようになっています。
一案として、「その他のプロパティ」を表すワイルドカードを導入して次のように書いてはどうでしょうか。
{ "name" : string, "age" : integer, "mailAddress" : string, "otherContacts" : [string*], * : any }
追加のプロパティを認めないときは、additionalPropertiesの値をfalseにするのですが、* : false
は気持ち悪いので、値が存在しない型neverを導入して、次のような感じが僕の好みです。
{ "name" : string, "age" : integer, "mailAddress" : string, "otherContacts" : [string*], * : never }
これでもまだ、ワイルドカード・プロパティを書かなかったときの解釈が未定です。このへんは自転車置き場の議論になりそうで頭が痛いのですが、何も書かないときは * : any
があるとみなしましょう。
スキーマ属性の記述
「JSONの可能性がグンと拡がるぞ! JSONスキーマ」で紹介したように、JSONスキーマ仕様にはいくつかのスキーマ属性が定義されています。例えばinteger型, number型ならば、minimum(最小値)、maximum(最大値)というスキーマ属性を指定できます。
スキーマ属性の指定は、次のように書けばいいでしょう。
integer(minimum = 0, maximum = 10)
名前付き引数による関数呼び出しと同じ構文です。
基本スカラー型に関しては、関数呼び出し風の構文でいいと思いますが、配列型やオブジェクト型のときはどうすべきでしょう。
[integer, string, any*](maxItems = 16)
ウーン、なんか落ち着きが悪い。
array(maxItems = 16) [integer, string, any*]
こっちのほうがいいですね。関数呼び出し形式に拘るなら:
array(maxItems = 16, items = [integer, string, any*])
となり、元のJSONスキーマにも近くなります。ですが、配列のitems属性とオブジェクトのproperties属性は例外的に外に出したほうが視認性が向上するようです。次はobjectの例です。
object { "name" : string(minLength = 2, maxLength = 20), "age" : integer(minimum = 0, maximum = 150), "mailAddress" : string(optional = true), "otherContacts" : array(optional = true) [any, any*], * : any }
この例は次のような内容を表現しています。
- nameプロパティの値は、長さ2以上20以下の文字列。
- ageプロパティの値は、0以上150以下の整数。
- mailAddressプロパティの値は、任意の文字列*1で、必須ではない(オプショナル)。
- otherContactsプロパティの値は、配列で、必須ではない。配列(があればそれ)の最初の項目は必須で型は任意、続く項目は個数もその型も任意。
- このオブジェクトには、指定された以外のプロパティがあってもよく、それら追加のプロパティの型は任意。
ユニオン型
ユニオン型はプログラムで扱いにくく、使いたくない場合も多いのですが、構文は決めておきましょう。
正規表現から星印を借りてきたので、同じく正規表現の記法を採用するなら、(integer | string)
となるでしょう。もう少し複雑な例を出すなら:
object { "name" : (string(minLength = 2, maxLength = 20) | object { "given" : string(minLength = 1, maxLength = 10), "family" : string(minLength = 1, maxLength = 10) } ), ...(省略)... }
その他の問題点
ユニオン型は扱いにくいと書きましたが、弁別子(タイプタグ)が付くと扱いがずっと楽になります。弁別子付きユニオン型の記述は次のようでしょうか。
case ( "simple" => string(minLength = 2, maxLength = 20) | "struct" => object { "given" : string(minLength = 1, maxLength = 10), "family" : string(minLength = 1, maxLength = 10) } )
ユーザーが定義した型に名前を付けたいという要望もありそうですね。BNF風にするなら、次のようかな。
name ::= (string(minLength = 2, maxLength = 20) | object { "given" : string(minLength = 1, maxLength = 10), "family" : string(minLength = 1, maxLength = 10) } );
今まで述べてきたような型定義構文は「難しすぎる」という意見もあるでしょうね(僕自身そう思っています)。もっと単純にする工夫はまたの機会に。
*1:実際には、メールアドレスフォーマットに制限すべきでしょう。しかし、そこまでスキーマで書くべきかどうかは疑問です。型チェックの一部は、処理プログラム側のアルゴリズムとして記述したほうが良い場合もあります。