🖐

5分で理解する JSON Type Definition

2021/11/09に公開

この記事は JSON Type Definition schema を理解するためのチュートリアルです.
もしかするとあなたは(中略)JTD が正式に定義されている RFC 8927 に興味を持っているかもしれません.
しかし多くの人にとっては,このチュートリアルのほうが理解しやすいでしょう.さっそく始めましょう!

JSON Type Definition schema とはなにか

"JSON Type Definition" とは,単なる JSON 文書です(JSON Typedef とか JTD とも呼びます).

{
  "properties": {
    "name": { "type": "string" },
    "isAdmin": { "type": "boolean" }
  }
}

これを YAML 形式で書くと,次のようになります:

properties:
  name:
    type: string
  isAdmin:
    type: boolean

技術的には JSON で書かれたスキーマのみが有効ですが,まずスキーマを YAML で書き,最後の最後で JSON に変換することはかなり一般的な手法でしょう.

また,スキーマには 8つの形式があります.あるスキーマがどの形式を採用しているかは,そのスキーマに含まれるキーワードからわかります.また,8つの形式とは以下のことを指しています:

eight forms

  • empty: Java でいう Object や Typescript でいう any を扱う形式

  • type: Java や Typescript でいう primitive type を扱う形式

  • enum: Java や Typescript でいう enum を扱う形式

  • elements: Java でいう List<T> や Typescript でいう T[] を扱う形式

  • properties: Java でいう Class や Typescript でいう interface を扱う形式

  • values: Java でいう Map<String, T> や Typescript でいう { [key: string]: T} を扱う形式

  • discriminator: タグ付き Union を扱う形式[1]

  • ref: スキーマを再利用するためのもの.通常は同じことを繰り返さないようにする.

スキーマは,これらのうちどれか一つに正確に従わねばなりません.すなわち,ある形式のキーワードと他の形式のキーワードを混在させることはできません.

Empty

次のスキーマは有効です:

schema
{}

これは「空」のスキーマです.あらゆる JSON の値を受け入れ,いかなるものも拒否しません.

Type

スキーマで type を使うと,ある値がプリミティブな JSON であることを指定できます.たとえば,次のように表せます.

schema
{ "type": "boolean" }

true あるいは false のみ受け入れ,その他のすべては拒否します.

以下に,type に設定できるすべての値を紹介します.

type What it accepts Example
boolean true or false true
string JSON strings "foo"
timestamp JSON strings containing an RFC3339 timestamp "1985-04-12T23:20:50.52Z"
float32 JSON numbers 3.14
float64 JSON numbers 3.14
int8 Whole JSON numbers that fit in a signed 8-bit integer 127
uint8 Whole JSON numbers that fit in an unsigned 8-bit integer 255
int16 Whole JSON numbers that fit in a signed 16-bit integer 32767
uint16 Whole JSON numbers that fit in an unsigned 16-bit integer 65535
int32 Whole JSON numbers that fit in a signed 32-bit integer 2147483647
uint32 Whole JSON numbers that fit in an unsigned 32-bit integer 4294967295

Enum

enum を使うと,あるリストに含まれる文字列であるとすることができます.

schema
{ "enum": ["FOO", "BAR", "BAZ"] }

"FOO"、"BAR"、"BAZ "のみ受け入れ,それ以外は拒否します.ただし,JTD では〈文字列の列挙のみが可能〉で,数字の列挙はできません

Elements

配列を記述するには elements を使います. elements の値となるのは 別の JTD スキーマ です.

schema
{ "elements": { "type": "string" } }

各要素が文字列である配列を受け付けます.つまり,["foo", "bar"] [] は OK ですが,"foo" (文字列型)と [1, 2, 3] (数値型の配列)は NG です.

Properties

各キーが別々の型を持つの値がある JSON オブジェクトを記述するには, properties スキーマを使います.

schema
{
  "properties": {
    "name": { "type": "string" },
    "isAdmin": { "type": "boolean" }
  }
}

この例では,name プロパティ(文字列でなければならない)と isAdmin プロパティ(ブール値でなければならない)を持つオブジェクトを受け入れます.
もしオブジェクトに余分なプロパティがある場合は無効となります.よって,次のようなオブジェクトの場合では OK です.

OK
{ "name": "Abraham Lincoln", "isAdmin": true }

しかし,次のような場合では NG です.

NG; isAdmin must be boolean
{ "name": "Abraham Lincoln", "isAdmin": "yes" }
NG; extra key is invalid
{ "name": "Abraham Lincoln", "isAdmin": true, "extra": "stuff" }

Optional properties

プロパティが欠けていてもいいのであれば, optionalProperties を使うことができます.

schema
{
  "properties": {
    "name": { "type": "string" },
    "isAdmin": { "type": "boolean" }
  },
  "optionalProperties": {
    "middleName": { "type": "string" }
  }
}

例えば,もしオブジェクトに middleName プロパティが含まれていれば,その値は文字列でなければなりません.しかし,そもそも middleName プロパティが含まれていなかった場合,それはそれとして OK と見做されます.
つまり,次のような場合は OK であると言えます:

OK
{ "name": "Abraham Lincoln", "isAdmin": true }
OK
{ "name": "William Sherman", "isAdmin": false, "middleName": "Tecumseh" }

一方で,次の場合はダメです:

NG
{ "name": "John Doe", "isAdmin": false, "middleName": null }

Extra properties

デフォルトでは,properties / optionalProperties は「追加」プロパティ,つまり〈スキーマで明示的に言及されていないプロパティ〉を許可しません.

余分なプロパティがあっても構わない場合は,"additionalProperties": true を使用してください.例えば次のように使います:

schema
{
  "properties": {
    "name": { "type": "string" },
    "isAdmin": { "type": "boolean" }
  },
  "additionalProperties": true
}

これは,次に示すオブジェクトを受け入れます:

OK
{ "name": "Abraham Lincoln", "isAdmin": true, "extra": "stuff" }

Values

「キーはわからないが値のタイプはわかる」という辞書のような JSON オブジェクトを記述するには,values を使います.

values をキーとしたときの値は,別の JTD スキーマとなります.

schema
{ "values": { "type": "boolean" } }

この例では,すべての値が boolean であるオブジェクトを受け入れます.つまり,{} とか {"a": true, "b": false} は受け入れますが,{"a": 1} は拒否します.

Discriminator

タグ付きユニオン[2]のように機能する JSON オブジェクトを記述するには, discriminator スキーマを使用します。

discriminator スキーマには2つのキーワードがあります:

  • discriminator: どのプロパティが tag プロパティであるかを教える役割
  • mapping: tag プロパティの値に基づいて,どのスキーマを使用するかを示す役割

例えば,次のような「メッセージ」があったとします:

examples of message
{ "eventType": "USER_CREATED", "id": "users/123" }
{ "eventType": "USER_CREATED", "id": "users/456" }

{ "eventType": "USER_PAYMENT_PLAN_CHANGED", "id": "users/789", "plan": "PAID" }
{ "eventType": "USER_PAYMENT_PLAN_CHANGED", "id": "users/123", "plan": "FREE" }

{ "eventType": "USER_DELETED", "id": "users/456", "softDelete": false }

これらの「メッセージ」には,基本的に3つの種類があるとします.例えば USER_CREATED の実体は次のように表すとします:

USER_CREATED
{
  "properties": {
    "id": { "type": "string" }
  }
}

USER_PAYMENT_PLAN_CHANGED, USER_DELETED についても同様に表現されているとしましょう:

USER_PAYMENT_PLAN_CHANGED
{
    "properties": {
        "id": { "type": "string" },
        "plan": { "enum": ["FREE", "PAID"]}
    }
}

USER_DELETED
{
    "properties": {
        "id": { "type": "string" },
        "softDelete": { "type": "boolean" }
    }
}

discriminator スキーマを使用すると,これら3つのスキーマをすべて結びつけることができ,さらに(この例の場合) eventType の値に基づいてどれが関連しているかを JTD に伝えることができます.

上述のいくつかの「メッセージ」に対するスキーマは次のとおりです:

schema
{
  "discriminator": "eventType",
  "mapping": {
    "USER_CREATED": {
      "properties": {
        "id": { "type": "string" }
      }
    },
    "USER_PAYMENT_PLAN_CHANGED": {
      "properties": {
        "id": { "type": "string" },
        "plan": { "enum": ["FREE", "PAID"] }
      }
    },
    "USER_DELETED": {
      "properties": {
        "id": { "type": "string" },
        "softDelete": { "type": "boolean" }
      }
    }
  }
}

このスキーマは,例に挙げたすべての「メッセージ」を受け入れます.

もし入力が eventType プロパティを持っていなかったり, eventType プロパティが mapping で記述されている3つの値のうちの一つでない場合,入力は拒否されます.

mapping 内に直にスキーマを記述できるのは, properties, optionalProperties, additionalProperties だけです.

他の種類のスキーマを mapping 内に使用することはできません.もし使うと "物事が曖昧になってしまう" からです.

Ref

あるサブスキーマを複数回再利用したい場合や,あるサブスキーマに特定の名前を付けたい場合があります.このような場合には,ref スキーマを使用することができます.

例を挙げて説明するのが一番簡単です,次のスキーマをみてください:

schema
{
    "definitions": {
        "coordinates": {
            "properties": {
                "lat": { "type": "float32" },
                "lng": { "type": "float32" }
            }
        }
    },
    "properties": {
        "userLoc": { "ref": "coordinates" },
        "serverLoc": { "ref": "coordinates" }
    }
}

このスキーマは次のオブジェクトを受け入れます:

OK
{ "userLoc": { "lat": 50, "lng": -90 }, "serverLoc": { "lat": -15, "lng": 50 }}

{"ref": "coordinates"} という部分は,基本的に definitions 内にある coordinates スキーマで「置き換え」られます.

definitions は JTD スキーマのルート(トップレベル)にしか現れないことに注意してください.それ以外の場所で definitions を持ってはいけません.

The nullable keyword

どんなスキーマでも,すなわちそれがいかなる「形式」であっても,nullable を付けることができます.また,それによって null がそのスキーマで受け入られうる値となります.

schema
{ "type": "string" }

これは,"foo" を受け入れますが,null は拒否します.しかし,"nullable": true を付け足すことで……,

schema
{ "type": "string", "nullable": true }

このスキーマは "foo"null の両方を受け入れるようになります.

The metadata keyword

metadata キーワードはどのスキーマでも有効ですが,もし存在する場合は JSON オブジェクトでなければなりません.また,metadatavalidation に影響を与えません.

通常,metadata はコードジェネレータのための説明やヒントなど,ツールが利用できるようなものを置くためのものであることに留意してください.

That’s it

あなたが生産的であるために JTD について知っておくべきことは,これですべてです!

もし JTD を使い始めようと思ってくれたなら,次のステップは〈自分の好きなプログラミング言語での実装を見つけること〉となるでしょう.

Next step

以下では JSON Type Definition の各プログラミング言語における実装事例をまとめています.

JSON Typedef は open standard です.すなわち,正式な規格を読んだり独自に実装したりといったことが,誰でも自由に行なえます.

Validation

JavaScript / TypeScript

  • jtd
    • npm パッケージとして提供されています
    • TypeScript 製であり,ブラウザでも Node.js でも動作します
  • ajv
    • npm パッケージとして提供されています
    • v7.1.0 から validation にも対応しています

Python

  • jtd
    • PyPI パッケージとして提供されています

Java

  • com.jsontypedef.jtd
    • Maven Central のパッケージとして提供されています
    • Jackson と Gson とともに動作します

Go

  • json-typedef-go
    • Go のモジュールとして GitHub 上で提供されています

C#

  • Jtd.Jtd
    • NuGet のパッケージとして提供されています
    • System.Text.JsonNewtonsoft.Json とともに動作します

Ruby

  • jtd
    • RubyGems における gem として提供されています

Rust

  • jtd
    • crates.io におけるパッケージとして提供されています
    • serde / serde_json とともに動作します

Code Generation

  • jtd-codegen
    • 上で紹介したすべての言語(TypeScript, Python, Java, Go, C#, Ruby , Rust)をサポートしています

Refference


補遺

JTD を活用した Ajv による 自動的な型定義・検証関数生成の話
https://zenn.dev/ningensei848/articles/getting-started-with-ajv-on-jtd

JSON Typed Definition Validator: 今すぐ JTD を試したいあなたに
https://jtd-validator.vercel.app/

脚注
  1. cf. TypeScript 4.6 で起こるタグ付きユニオンのさらなる進化 | Zenn ↩︎

  2. 別名として,判別可能な Union 型 (discriminated union),直和型(sum type) 等が挙げられる ↩︎

GitHubで編集を提案

Discussion