RFC 8927: JSON Type Definition を読んだ

2021-01-11

RFC Editor を眺めていたら JTD と呼ばれるものを見つけたので読んでみました。CDDL を調べながら原著を読めばわかるので自分なりに理解したことをまとめる感じの記事です。

RFC 8927: JSON Type Definition

全体の構成

トップレベルの構造は root-schema といい、 任意のメンバ definitions と必須のメンバ schema から成ります (schema の定義については後述します)。
CDDL の代わりに無理矢理 TypeScript っぽく書くと(?)次のような形です。

RootSchema: {
  definitions?: { [key: string]?: Schema, [key: string]?: Schema, ... },
  schema: Schema,
}

Schema

schema は JTD の主要な部分を表現する型として頻繁に登場します。 8つの form と呼ばれる塊をグループ化して名前をつけたもので、実態としては以下のように ref, type, enum, ... のいずれかです(// は定義を展開しつつ候補からの選択を表現する CDDL の演算子)。

schema = (
  ref //
  type //
  enum //
  elements //
  properties //
  values //
  discriminator //
  empty //
)

以降の節では、それぞれの form の定義について書き下していきます。

Empty

empty は TypeScript で言うところの any にあたるものだと思います。原著の 3.3.1. Empty では The "empty" form is meant to describe instances whose values are unknown, unpredictable, or otherwise unconstrained by the schema. と説明されています。

名前は empty ですが、 shared と呼ばれるオプション用のオブジェクトを渡すことができるため、完全な空オブジェクトではないパターンも許容します。

// schema
// OK
{}
{ "nullable": true }
{ "nullable": false, "metadata": { "some": "metadata" } }

// NG
{ "nullable": "apple" }

Ref

ref はルートのスキーマの定義を参照することを表します。前提として root-schema 上に definitions が存在することが必要です。

定義的には次のようなものも通ってしまうのですが、基本的には properties など高級な form の中で使うものだと思います。
2021/3 追記: 作り上 schema を validate した時に初めて検証されるので、上記は誤りです。 様々なパターンについては jsontypedef/json-typedef-spec を見た方が良さそうです。

# used a ruby implementation:
#  https://github.com/jsontypedef/json-typedef-ruby
schema = JTD::Schema.from_hash({ "definitions" => { foo: {} }, "ref" => "foo" })
# => #<JTD::Schema:0x00...>
// schema
{
  "definitions": {
    "coordinates": {
      "properties": {
        "lat": { "type": "float32" },
        "lng": { "type": "float32" }
      }
    }
  },
  "properties": {
    "user_location": { "ref": "coordinates" },
    "server_location": { "ref": "coordinates" }
  }
}

Type

type は一般的な静的型付け言語にもあるような値の型で、それぞれの型に対する具体的な説明は原著にあります。
型そのものに対しては説明することはないのですが、float に対応する説明には a JSON number とあり、これに関しては知らなかったことがたくさんありました。

Enum

enum は列挙型です。要素数1以上の文字列のリストとして値の候補を与えることができます。

// schema
// OK
{ "enum": ["apple", "banana"] }

// NG
{ "enum": [] }

Elements

elements は与えた schema を要素とする配列であることを定義します。

あるオブジェクトの中の v の値に、 enum に含まれる文字列の配列を期待する場合の例はこんな感じです。

schema = JTD::Schema.from_hash({
  'properties' => {
    'v' => {
      'elements' => { 'enum' => ['foo', 'bar', 'baz'] }
    }
  }
})

# エラーがなければ空配列が返る
JTD::validate(schema, {
  'v' => ['foo', 'bar']
})
# => []

Properties

properties は任意の名前のキーに対して任意の schema が対応するメンバを複数持つ構造体ライクなオブジェクトを表現します。
メンバは必須または任意として定義でき、さらに追加でメンバを持たせることを許容できます。 必須と任意のメンバに重複するキーを存在させることはできません。

// schema
// OK
{
  "properties": {
    "users": {
      "elements": {
        "properties": {
          "id": { "type": "string" },
          "name": { "type": "string" },
          "created_at": { "type": "timestamp" }
        },
        "optionalProperties": {
          "updated_at": { "type": "timestamp" }
        }
      }
    }
  }
}

// NG
{
  "properties": { "conflict_key": {} },
  "optionalProperties": { "conflict_key": {} }
}

Values

values は任意の名前のキーに対して単一の schema が対応する要素を格納する連想配列ライクなオブジェクトを表現します。 値の型が同一になる Hash(Ruby) か Object(JavaScript) みたいなイメージで捉えています。

// schema
{
  "values": {
    "type": "float32"
  }
}

// instance
// OK
{}
{ "a": 1, "b": 2 }

// NG
null
{ "a": 1, "b": 2, "c": "foo" }

Discriminator

discriminator は Discriminated Union を記述するためのもので、 Polymorphism を表現できるのがいいみたいです。この記事がわかりやすかったです。

スキーマとインスタンスに登場するメンバを、原著中の D, M, I, S に対応させると、次のようになります。

  • D : schema において discriminator として定義された値、 "何をオブジェクトの識別に使うのか" に対しての名前
  • M : schema における mapping に対応する値、オブジェクトの候補
  • I : instance における D と同名のメンバ
  • S : I と同名の M のメンバ
// schema
{
  "discriminator": "version", // D
  "mapping": { // M
    "v1": {
      "properties": {
        "a": { "type": "float32" }
      }
    },
    "v2": { // S
      "properties": {
        "a": { "type": "string" }
      }
    }
  }
}

// instance
{
  "version": "v2", // I
  "a": "foo"
}

まとめ

JSON Type Definition の RFC を読んで、自分なりにまとめました。
より細かいルールや実装系のことを考えるには必須のエラーの構造などには触れませんでしたが、再帰的な表現を多用することで言語仕様としてコンパクトにまとまっているなという印象でした。

個人的には RFC ちゃんと読むの初めてだったので勉強になりました。 JTD はまだ Experimental というカテゴリにあるものでしたが、すでに標準になっている定番にもチャレンジしたいです。

参考・関連記事