リファレンス

複雑なスキーマの構造化

中程度の複雑さのコンピュータプログラムを作成する場合、プログラムを再利用可能な関数に「構造化」する方が、使用する場所ごとに重複したコードをコピー&ペーストするよりも優れていることが一般的に受け入れられています。同様にJSON Schemaでも、最も単純なスキーマ以外では、スキーマを多数の場所で再利用できるパーツに構造化すると非常に便利です。この章では、スキーマを再利用および構造化するためのツールと、それらのツールを使用する実践的な例を紹介します。

スキーマ識別

他のコードと同様に、スキーマは必要に応じて相互参照する論理ユニットに分割できる場合に保守が容易になります。スキーマを参照するには、スキーマを識別する方法が必要です。スキーマドキュメントは、非相対URIによって識別されます。

スキーマドキュメントに識別子を付ける必要はありませんが、あるスキーマから別のスキーマを参照する場合は識別子が必要になります。このドキュメントでは、識別子のないスキーマを「匿名スキーマ」と呼びます。

次のセクションでは、スキーマの「識別子」がどのように決定されるかを見ていきます。

URIの用語は直感的でない場合があります。このドキュメントでは、次の定義を使用します。

  • URI [1] または 非相対URI:スキーム(https)を含む完全なURI。URIフラグメント(#foo)を含む場合があります。このドキュメントでは、相対URIが許可されていないことを明確にするために、「非相対URI」を使用する場合があります。
  • 相対参照 [2]:スキーム(https)を含まない部分的なURI。フラグメント(#foo)を含む場合があります。
  • URI参照 [3]:相対参照または非相対URI。URIフラグメント(#foo)を含む場合があります。
  • 絶対URI [4]:スキーム(https)を含む完全なURIですが、URIフラグメント(#foo)は含みません。

スキーマは URI で識別されますが、これらの識別子は必ずしもネットワークアドレス可能である必要はありません。それらは単なる識別子です。一般的に、実装は、スキーマを取得するために HTTP リクエスト(https://)を行ったり、ファイルシステム(file://)から読み取ったりすることはありません。代わりに、スキーマを内部スキーマデータベースにロードする方法を提供します。スキーマが URI 識別子で参照されると、そのスキーマは内部スキーマデータベースから取得されます。

ベースURI

非相対 URI を使用するのは面倒な場合があるため、JSON Schema で使用される URI は、スキーマのベース URI に対して解決される URI 参照であり、その結果、非相対 URI になります。このセクションでは、スキーマのベース URI がどのように決定されるかについて説明します。

ベース URI の決定と相対参照の解決は、RFC-3986 で定義されています。これが HTML でどのように機能するかを理解していれば、このセクションは非常によく似ていると感じるはずです。

取得 URI

スキーマを取得するために使用される URI は「取得 URI」と呼ばれます。実装に匿名スキーマを渡すことはよくあり、その場合、そのスキーマには取得 URI がありません。

スキーマが URI https://example.com/schemas/address を使用して参照され、次のスキーマが取得されると仮定しましょう。

スキーマ
{ "type": "object", "properties": { "street_address": { "type": "string" }, "city": { "type": "string" }, "state": { "type": "string" } }, "required": ["street_address", "city", "state"]}

このスキーマのベース URI は、取得 URI である https://example.com/schemas/address と同じです。

$id

スキーマのルートで $id キーワードを使用することで、ベース URI を設定できます。$id の値は、取得 URI に対して解決されるフラグメントのない URI 参照です。結果の URI がスキーマのベース URI になります。

ドラフト固有の情報:
ドラフト 4
ドラフト 4-7
ドラフト 4 では、$id は単に id(ドル記号なし)です。

これは、HTML の <base> タグに類似しています。

$id キーワードがサブスキーマに現れる場合、その意味は少し異なります。詳しくはバンドリングのセクションをご覧ください。

URI https://example.com/schema/addresshttps://example.com/schema/billing-address が両方とも以下のスキーマを識別すると仮定しましょう。

スキーマ
{ "$id": "/schemas/address",
"type": "object", "properties": { "street_address": { "type": "string" }, "city": { "type": "string" }, "state": { "type": "string" } }, "required": ["street_address", "city", "state"]}

このスキーマを取得するためにどちらのURIが使われたとしても、ベースURIは https://example.com/schemas/address となります。これは、取得URIに対して $id URI参照を解決した結果です。

しかし、ベースURIを設定する際に相対参照を使用すると、問題が発生する可能性があります。例えば、このスキーマを匿名スキーマとして使用することはできません。なぜなら、取得URIが存在せず、何もないものに対して相対参照を解決することはできないからです。このため、そして他の理由からも、$id を使用してベースURIを宣言する際には、常に絶対URIを使用することが推奨されます。

以下のスキーマのベースURIは、取得URIが何であるか、あるいは匿名スキーマとして使用されているかに関わらず、常に https://example.com/schemas/address となります。

スキーマ
{ "$id": "https://example.com/schemas/address",
"type": "object", "properties": { "street_address": { "type": "string" }, "city": { "type": "string" }, "state": { "type": "string" } }, "required": ["street_address", "city", "state"]}

JSONポインター

スキーマドキュメントを識別することに加えて、サブスキーマを識別することもできます。最も一般的な方法は、サブスキーマを指すURIフラグメントでJSONポインターを使用することです。

JSONポインターは、ドキュメント内のオブジェクトのキーをたどるためのスラッシュで区切られたパスを記述します。したがって、/properties/street_address は、以下の意味になります。

  • 1) キーpropertiesの値を見つける
  • 2) そのオブジェクト内で、キーstreet_addressの値を見つける

URI https://example.com/schemas/address#/properties/street_addressは、次のスキーマで強調表示されたサブスキーマを識別します。

スキーマ
{ "$id": "https://example.com/schemas/address", "type": "object", "properties": { "street_address": { "type": "string" }, "city": { "type": "string" }, "state": { "type": "string" } }, "required": ["street_address", "city", "state"]}

アンカー

サブスキーマを識別するあまり一般的でない方法は、$anchor キーワードを使用してスキーマに名前付きアンカーを作成し、その名前をURIフラグメントで使用することです。アンカーは、先頭が文字で、その後に任意の数の文字、数字、-_:、または.が続く必要があります。

ドラフト固有の情報:
ドラフト 4
ドラフト 6-7

ドラフト4では、$idが(ドル記号なしの)単なるidである点を除いて、ドラフト6-7と同じ方法でアンカーを宣言します。

これらの命名規則に従わない名前付きアンカーが定義されている場合、動作は未定義です。アンカーは一部の実装では機能するかもしれませんが、他の実装では機能しない可能性があります。

URI https://example.com/schemas/address#street_address は、次のスキーマのハイライトされた部分のサブスキーマを識別します。

スキーマ
{ "$id": "https://example.com/schemas/address", "type": "object", "properties": { "street_address": { "$anchor": "street_address", "type": "string" }, "city": { "type": "string" }, "state": { "type": "string" } }, "required": ["street_address", "city", "state"]}

$ref

スキーマは、$refキーワードを使用して別のスキーマを参照できます。$refの値は、スキーマのベース URIに対して解決されるURI参照です。$refを評価する際、実装は解決された識別子を使用して参照されるスキーマを取得し、そのスキーマをインスタンスに適用します。

ドラフト固有の情報
Draft 4-7では、$refの動作が少し異なります。オブジェクトに$refプロパティが含まれている場合、オブジェクトはスキーマではなく参照と見なされます。したがって、そのオブジェクトに記述した他のプロパティはJSONスキーマのキーワードとして扱われず、バリデーターによって無視されます。$refは、スキーマが期待される場所でのみ使用できます。

この例では、顧客ごとに配送先住所と請求先住所の両方を持つ顧客レコードを定義したいとします。住所は常に同じ(番地、市区町村、都道府県)であるため、住所を保存するたびにスキーマのその部分を複製したくありません。スキーマが冗長になるだけでなく、将来の更新も困難になります。もし私たちの架空の会社が将来海外ビジネスを始め、すべての住所に国フィールドを追加したい場合、住所が使用されているすべての場所ではなく、単一の場所でこれを行う方が良いでしょう。

スキーマ
{ "$id": "https://example.com/schemas/customer",
"type": "object", "properties": { "first_name": { "type": "string" }, "last_name": { "type": "string" }, "shipping_address": { "$ref": "/schemas/address" }, "billing_address": { "$ref": "/schemas/address" } }, "required": ["first_name", "last_name", "shipping_address", "billing_address"]}

スキーマ
{ "type": "object", "properties": { "first_name": { "type": "string" }, "last_name": { "type": "string" }, "shipping_address": { "$ref": "https://example.com/schemas/address" }, "billing_address": { "$ref": "/schemas/address" } }, "required": ["first_name", "last_name", "shipping_address", "billing_address"]}

/properties/shipping_address$ref は、相対的でないベース URI がなくても正常に解決できますが、/properties/billing_address$ref は相対的でない URI に解決できないため、アドレススキーマを取得するために使用できません。

$defs

時には、現在のスキーマでのみ使用することを意図した小さなサブスキーマがあり、それらを個別のスキーマとして定義することは意味がありません。JSON ポインターまたは名前付きアンカーを使用して任意のサブスキーマを識別できますが、$defs キーワードを使用すると、現在のスキーマドキュメント内で再利用することを意図したサブスキーマを保持するための標準化された場所が提供されます。

前の顧客スキーマの例を拡張して、名前プロパティに共通のスキーマを使用してみましょう。このために新しいスキーマを定義することは意味がなく、このスキーマでのみ使用されるため、$defs を使用するのに適しています。

スキーマ
{ "$id": "https://example.com/schemas/customer",
"type": "object", "properties": { "first_name": { "$ref": "#/$defs/name" }, "last_name": { "$ref": "#/$defs/name" }, "shipping_address": { "$ref": "/schemas/address" }, "billing_address": { "$ref": "/schemas/address" } }, "required": ["first_name", "last_name", "shipping_address", "billing_address"],
"$defs": { "name": { "type": "string" } }}

$defs は、重複を避けるためにのみ役立つわけではありません。また、読みやすく、保守しやすいスキーマを作成するのに役立ちます。スキーマの複雑な部分は、$defs に記述的な名前で定義し、必要な場所で参照できます。これにより、スキーマの読者は、より複雑な部分に飛び込む前に、スキーマをより迅速かつ容易に高レベルで理解できます。

外部サブスキーマを参照することは可能ですが、一般的に、$ref は、外部スキーマまたは $defs で定義された内部サブスキーマのいずれかを参照するように制限する必要があります。

再帰

$ref キーワードは、それ自体を参照する再帰的スキーマを作成するために使用できます。たとえば、person スキーマに children の配列があり、それぞれが person インスタンスである場合があります。

スキーマ
{ "type": "object", "properties": { "name": { "type": "string" }, "children": { "type": "array", "items": { "$ref": "#" } } }}

イギリス王室の家系図の一部

データ
{ "name": "エリザベス", "children": [ { "name": "チャールズ", "children": [ { "name": "ウィリアム", "children": [ { "name": "ジョージ" }, { "name": "シャーロット" } ] }, { "name": "ハリー" } ] } ]}
スキーマに準拠しています

上記では、自分自身を参照するスキーマを作成しました。これは、バリデーター内で事実上「ループ」を作成することになり、許可されており、有用でもあります。ただし、別の$refを参照する$refは、リゾルバー内で無限ループを引き起こす可能性があり、明示的に許可されていません。

スキーマ
{ "$defs": { "alice": { "$ref": "#/$defs/bob" }, "bob": { "$ref": "#/$defs/alice" } }}

再帰的なスキーマの拡張

ドラフト2019-09の新機能

ドキュメントは近日公開予定

バンドル

複数のスキーマドキュメントを扱うことは開発に便利ですが、すべてのスキーマを1つのスキーマドキュメントにバンドルする方が配布には便利なことがよくあります。これは、サブスキーマ内の$idキーワードを使用して行うことができます。$idがサブスキーマで使用されている場合、それは埋め込みスキーマを示します。埋め込みスキーマの識別子は、それが現れるスキーマのBase URIに対して解決された$idの値です。埋め込みスキーマを含むスキーマドキュメントは、複合スキーマドキュメントと呼ばれます。複合スキーマドキュメント内の$idを持つ各スキーマは、スキーマ リソースと呼ばれます。

ドラフト固有の情報:
ドラフト 4
ドラフト 4-7
ドラフト 4 では、$id は単に id(ドル記号なし)です。

これは、HTMLの<iframe> タグに似ています。

スキーマを開発する際に埋め込みスキーマを使用することはまれです。一般的に、この機能を明示的に使用せず、そのようなものが必要な場合はスキーマバンドルツールを使用してバンドルされたスキーマを構築するのが最適です。::

この例は、顧客スキーマの例と住所スキーマの例が複合スキーマドキュメントにバンドルされている様子を示しています。

スキーマ
{ "$id": "https://example.com/schemas/customer", "$schema": "https://json-schema.dokyumento.jp/draft/2020-12/schema",
"type": "object", "properties": { "first_name": { "type": "string" }, "last_name": { "type": "string" }, "shipping_address": { "$ref": "/schemas/address" }, "billing_address": { "$ref": "/schemas/address" } }, "required": ["first_name", "last_name", "shipping_address", "billing_address"],
"$defs": { "address": { "$id": "https://example.com/schemas/address", "$schema": "https://json-schema.dokyumento.jp/draft-07/schema#",
"type": "object", "properties": { "street_address": { "type": "string" }, "city": { "type": "string" }, "state": { "$ref": "#/definitions/state" } }, "required": ["street_address", "city", "state"],
"definitions": { "state": { "enum": ["CA", "NY", "... etc ..."] } } } }}

複合スキーマドキュメント内のすべての参照は、スキーマリソースがバンドルされているかどうかに関わらず同じである必要があります。顧客スキーマからの$refキーワードは変更されていないことに注意してください。唯一の違いは、住所スキーマが別のスキーマドキュメントではなく、/$defs/addressで定義されるようになったことです。スキーマをバンドル解除した場合、その参照が住所スキーマを指さなくなるため、#/$defs/addressを使用して住所スキーマを参照することはできませんでした。

ドラフト固有の情報
ドラフト4-7では、サブスキーマの$idはベースURIの変更のみを表し、埋め込みスキーマを表していなかったため、これらのURIはどちらも有効です。ただし、許可されていても、JSONポインターがベースURIが変更されたスキーマをまたがないようにすることを強くお勧めします。

"$ref": "#/definitions/state"は、埋め込みスキーマが使用されていない場合のように、トップレベルスキーマのdefinitionsキーワードではなく、住所スキーマのdefinitionsキーワードに解決されることも確認する必要があります。

各スキーマリソースは個別に評価され、異なるJSONスキーマのダイアレクトを使用する場合があります。上記の例では、住所スキーマリソースがドラフト7を使用している一方で、顧客スキーマリソースはドラフト2020-12を使用しています。埋め込みスキーマで$schemaが宣言されていない場合、デフォルトでは親スキーマのダイアレクトを使用します。

ドラフト固有の情報
ドラフト4-7では、サブスキーマの$idはベースURIの変更に過ぎず、独立したスキーマリソースとは見なされません。$schemaはスキーマリソースのルートでのみ許可されているため、サブスキーマの$idを使用してバンドルされたすべてのスキーマは、同じダイアレクトを使用する必要があります。
ドラフト固有の情報
ドラフト2020-12では、埋め込みスキーマでのダイアレクトの変更(親スキーマとは異なる値を持つ$schemaを使用)のサポートが追加されました。

お困りですか?

これらのドキュメントは役に立ちましたか?

ドキュメントを素晴らしいものにするためにご協力ください!

JSON Schemaでは、ドキュメントの貢献を他のすべての貢献と同じくらい重要視しています!

まだお困りですか?

JSON Schemaの学習は混乱することが多いですが、ご心配なく。私たちがサポートします!