5分で理解する JSON Type Definition
この記事は 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
次のスキーマは有効です:
{}
これは「空」のスキーマです.あらゆる JSON の値を受け入れ,いかなるものも拒否しません.
Type
スキーマで type
を使うと,ある値がプリミティブな JSON であることを指定できます.たとえば,次のように表せます.
{ "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
を使うと,あるリストに含まれる文字列であるとすることができます.
{ "enum": ["FOO", "BAR", "BAZ"] }
"FOO"、"BAR"、"BAZ "のみ受け入れ,それ以外は拒否します.ただし,JTD では〈文字列の列挙のみが可能〉で,数字の列挙はできません.
Elements
配列を記述するには elements
を使います. elements
の値となるのは 別の JTD スキーマ です.
{ "elements": { "type": "string" } }
各要素が文字列である配列を受け付けます.つまり,["foo", "bar"]
と []
は OK ですが,"foo"
(文字列型)と [1, 2, 3]
(数値型の配列)は NG です.
Properties
各キーが別々の型を持つの値がある JSON オブジェクトを記述するには, properties
スキーマを使います.
{
"properties": {
"name": { "type": "string" },
"isAdmin": { "type": "boolean" }
}
}
この例では,name
プロパティ(文字列でなければならない)と isAdmin
プロパティ(ブール値でなければならない)を持つオブジェクトを受け入れます.
もしオブジェクトに余分なプロパティがある場合は無効となります.よって,次のようなオブジェクトの場合では OK です.
{ "name": "Abraham Lincoln", "isAdmin": true }
しかし,次のような場合では NG です.
{ "name": "Abraham Lincoln", "isAdmin": "yes" }
{ "name": "Abraham Lincoln", "isAdmin": true, "extra": "stuff" }
Optional properties
プロパティが欠けていてもいいのであれば, optionalProperties
を使うことができます.
{
"properties": {
"name": { "type": "string" },
"isAdmin": { "type": "boolean" }
},
"optionalProperties": {
"middleName": { "type": "string" }
}
}
例えば,もしオブジェクトに middleName
プロパティが含まれていれば,その値は文字列でなければなりません.しかし,そもそも middleName
プロパティが含まれていなかった場合,それはそれとして OK と見做されます.
つまり,次のような場合は OK であると言えます:
{ "name": "Abraham Lincoln", "isAdmin": true }
{ "name": "William Sherman", "isAdmin": false, "middleName": "Tecumseh" }
一方で,次の場合はダメです:
{ "name": "John Doe", "isAdmin": false, "middleName": null }
Extra properties
デフォルトでは,properties
/ optionalProperties
は「追加」プロパティ,つまり〈スキーマで明示的に言及されていないプロパティ〉を許可しません.
余分なプロパティがあっても構わない場合は,"additionalProperties": true
を使用してください.例えば次のように使います:
{
"properties": {
"name": { "type": "string" },
"isAdmin": { "type": "boolean" }
},
"additionalProperties": true
}
これは,次に示すオブジェクトを受け入れます:
{ "name": "Abraham Lincoln", "isAdmin": true, "extra": "stuff" }
Values
「キーはわからないが値のタイプはわかる」という辞書のような JSON オブジェクトを記述するには,values
を使います.
values
をキーとしたときの値は,別の JTD スキーマとなります.
{ "values": { "type": "boolean" } }
この例では,すべての値が boolean であるオブジェクトを受け入れます.つまり,{}
とか {"a": true, "b": false}
は受け入れますが,{"a": 1}
は拒否します.
Discriminator
タグ付きユニオン[2]のように機能する JSON オブジェクトを記述するには, discriminator
スキーマを使用します。
discriminator
スキーマには2つのキーワードがあります:
-
discriminator
: どのプロパティが tag プロパティであるかを教える役割 -
mapping
: tag プロパティの値に基づいて,どのスキーマを使用するかを示す役割
例えば,次のような「メッセージ」があったとします:
{ "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 の実体は次のように表すとします:
{
"properties": {
"id": { "type": "string" }
}
}
USER_PAYMENT_PLAN_CHANGED, USER_DELETED についても同様に表現されているとしましょう:
{
"properties": {
"id": { "type": "string" },
"plan": { "enum": ["FREE", "PAID"]}
}
}
{
"properties": {
"id": { "type": "string" },
"softDelete": { "type": "boolean" }
}
}
discriminator
スキーマを使用すると,これら3つのスキーマをすべて結びつけることができ,さらに(この例の場合) eventType の値に基づいてどれが関連しているかを JTD に伝えることができます.
上述のいくつかの「メッセージ」に対するスキーマは次のとおりです:
{
"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
スキーマを使用することができます.
例を挙げて説明するのが一番簡単です,次のスキーマをみてください:
{
"definitions": {
"coordinates": {
"properties": {
"lat": { "type": "float32" },
"lng": { "type": "float32" }
}
}
},
"properties": {
"userLoc": { "ref": "coordinates" },
"serverLoc": { "ref": "coordinates" }
}
}
このスキーマは次のオブジェクトを受け入れます:
{ "userLoc": { "lat": 50, "lng": -90 }, "serverLoc": { "lat": -15, "lng": 50 }}
{"ref": "coordinates"}
という部分は,基本的に definitions
内にある coordinates
スキーマで「置き換え」られます.
definitions
は JTD スキーマのルート(トップレベル)にしか現れないことに注意してください.それ以外の場所で definitions
を持ってはいけません.
nullable
keyword
The どんなスキーマでも,すなわちそれがいかなる「形式」であっても,nullable
を付けることができます.また,それによって null
がそのスキーマで受け入られうる値となります.
{ "type": "string" }
これは,"foo"
を受け入れますが,null
は拒否します.しかし,"nullable": true
を付け足すことで……,
{ "type": "string", "nullable": true }
このスキーマは "foo"
と null
の両方を受け入れるようになります.
metadata
keyword
The metadata
キーワードはどのスキーマでも有効ですが,もし存在する場合は JSON オブジェクトでなければなりません.また,metadata
は validation に影響を与えません.
通常,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 パッケージとして提供されています
- PyPI パッケージとして提供されています
Java
-
com.jsontypedef.jtd
- Maven Central のパッケージとして提供されています
- Jackson と Gson とともに動作します
Go
-
json-typedef-go
- Go のモジュールとして GitHub 上で提供されています
- Go のモジュールとして GitHub 上で提供されています
C#
-
Jtd.Jtd
- NuGet のパッケージとして提供されています
-
System.Text.Json
とNewtonsoft.Json
とともに動作します
Ruby
-
jtd
- RubyGems における gem として提供されています
- RubyGems における gem として提供されています
Rust
-
jtd
- crates.io におけるパッケージとして提供されています
-
serde / serde_json
とともに動作します
Code Generation
-
jtd-codegen
- 上で紹介したすべての言語(TypeScript, Python, Java, Go, C#, Ruby , Rust)をサポートしています
Refference
- Learn JSON Typedef in 5 Minutes | JSON Type Definition
- Implementations of JSON Typedef | JSON Type Definition
-
RFC 8927: JSON Type Definition を読んだ - no24.org
- きちんと RFC 8927 に目を通した偉人によるまとめ
-
複数のサブスキーマを持つデータへの対応におけるスキーマ記述言語の比較 | IIJ Engineers Blog
-
JSON Schema
,OpenAPI
,CDDL
を踏まえたこれまでの流れがまとまっている
-
-
CDDL
について
補遺
JTD を活用した Ajv による 自動的な型定義・検証関数生成の話
JSON Typed Definition Validator: 今すぐ JTD を試したいあなたに
-
別名として,判別可能な Union 型 (
discriminated union
),直和型(sum type
) 等が挙げられる ↩︎
Discussion