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

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

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

参照用 記事

JSONスキーマとユーザーインターフェース

JSONスキーマはデータ構造の定義言語ですから、本来ユーザーインターフェースとは何の関係もないはずです。ですが、仕様(http://www.json-schema.org/)には、optionsというスキーマ属性があります。

optionsスキーマ属性はインスタンスの妥当性検証には影響しません(The "options" attribute does not affect validation)。ユーザーインターフェースへのヒントとなります(The "options" attribute can be used for user interfaces in conjunction with "enum" to define the labels for the possible values)。enumスキーマ属性と一緒に使うことが多いでしょうが、enumを常に要求するわけではありません。

次は、仕様書に載っているoptionsスキーマ属性の使用例です(原文にはtypoがあったので修正しました)。

{
 "type" : "integer",
 "enum" : [1, 2, 3],
 "options" : [
    {"value": 1, "label": "Small"},
    {"value": 2, "label": "Medium"},
    {"value": 3, "label": "Large"}
 ]
}

僕がでっちあげた代替構文(私家版スキーマ言語)で書くとこんなふうです。

SMLSelection = integer(
  enum = [1, 2, 3],
  options = [
    (value = 1, label = "Small"),
    (value = 2, label = "Medium"),
    (value = 3, label = "Large")
  ]
);

この情報から、例えば次のようなHTMLフォームを生成できます。

  <select name="sml">
    <option value="1">Small</option>
    <option value="2">Medium</option>
    <option value="3">Large</option>
  </select>

この場合、個々の値にラベルを付けることによって、ユーザーインターフェースに対してヒントを与えています。

しかし、ラベルは値にだけではなく、オブジェクト・プロパティや配列項目に対しても必要になります。次の例を見てください。

BasicUserInfo = object {
    "name" : string,
    "gender" : 
        integer(enum = [1, 2],
                options = [
                    (value = 1, label = "男性"),
                    (value = 2, label = "女性")
                ])
};

これから生成できるユーザーインターフェースは次のようなものでしょう。

  <div class="formField">
    Name:
    <input type="text" name="name" />
  </div>
  <div class="formField">
    Gender:
    <select name="gender">
      <option value="1">男性</option>
      <option value="2">女性</option>
    </select>
  </div>

ラベルには、オブジェクトのプロパティ名 name, gender をそのまま使ってますが、ここは日本語を使いたいところです。すべての型に対して、スキーマ属性labelを許すとすれば、ユーザーインターフェースの生成に役に立ちます。

BasicUserInfo = object(label = "お客様基本情報") {
    "name" : string(label = "お名前"),
    "gender" : 
        integer(label = "性別",
                enum = [1, 2],
                options = [
                    (value = 1, label = "男性"),
                    (value = 2, label = "女性")
                ])
};

labelで指定された情報を使えば、すべて日本語のユーザーインターフェースを構成できます。

  <fieldset><legend>お客様基本情報<legend>
    <div class="formField">
      お名前:
      <input type="text" name="name" />
    </div>
    <div class="formField">
      性別:
      <select name="gender">
        <option value="1">男性</option>
        <option value="2">女性</option>
      </select>
    </div>
  </fieldset>

これでも、スキーマ定義だけではやっぱり情報として不足です。例えば、ユーザーインターフェース生成時には次のような判断も必要です。

  • enumによる列挙値を、radioボタングループにするか、selectメニューにするか。
  • stringを、テキストフィールドにするかテキストエリアにするか。
  • 初期値をどうするか(デフォルト値と初期値が同じ概念とは限らない)。

これらの判断もスキーマ属性として記述することはできますが、やりすぎると「メンドクセー、直接ユーザーインターフェースを書いたほうがいいや」なんてことになりかねません。バランスが大事

さて、少し実用的な例として、ランチの注文フォームのスキーマを書いてみました。これに対応するHTMLフォームも手動で生成してみました(リンクをたどって見ることができます)。

生成作業を手動から自動にはできそうだと思うのですが、その前に、問題があります。下の私家版スキーマ言語によるスキーマ定義は、オリジナルのJSONスキーマに比べれば随分と見やすいと思います。しかしそれでも、ウゲーッって感じがしますよね。このウゲーッ感をなんとかしないと使いものになりませんね。

//
// 注意:
// スキーマ属性 label, unique, unordered は、
// オリジナルJSONスキーマにはありません。
//
LunchOrder =
  object(label = "ランチご注文") {
    "ident" : object(label = "ランチ会員情報") {
                "name" : string(label = "お名前"),
                "memberNum" : integer(label = "会員番号")
              },
    "order" : object(label = "本日のご注文") {
                "mainDish" : string(label = "メイン",
                                    enum = ["special", "fish", "meat"],
                                    options = [
                                      (value = "special", label = "日替わり"),
                                      (value = "fish", label = "魚"),
                                      (value = "meat", label = "肉")
                                     ]
                             ),
                "drink" : string(label = "お飲み物",
                                 enum = ["teaHot", "teaIce", 
                                         "coffeeHot", "coffeeIce"],
                                 options = [
                                      (value = "teaHot", 
                                       label = "紅茶 (ホット)"),
                                      (value = "teaIce", 
                                       label = "紅茶 (アイス)"),
                                      (value = "coffeeHot", 
                                       label = "コーヒー (ホット)"),
                                      (value = "coffeeIce",
                                       label = "コーヒー (アイス)")
                                     ]
                          ),
                "dessert" : array(label = "デザート",
                                  unique = true,
                                  unordered = true) 
                              [
                                string(
                                  enum = ["pudding", "strawberryShortcake",
                                         "bavarianCream", "gateauChocolat"],
                                  options = [
                                      (value = "pudding",
                                       label = "プリン"),
                                      (value = "strawberryShortcake", 
                                       label = "苺ショートケーキ"),
                                      (value = "bavarianCream", 
                                       label = "ババロア"),
                                      (value = "gateauChocolat",
                                       label = "ガトーショコラ")
                                  ]
                                ) *
                               ]
              } // 本日のご注文
  } // ランチご注文
;