リファレンス
複雑なスキーマの構造化
中程度の複雑さのコンピュータプログラムを作成する場合、プログラムを再利用可能な関数に「構造化」する方が、使用する場所ごとに重複したコードをコピー&ペーストするよりも優れていることが一般的に受け入れられています。同様にJSON Schemaでも、最も単純なスキーマ以外では、スキーマを多数の場所で再利用できるパーツに構造化すると非常に便利です。この章では、スキーマを再利用および構造化するためのツールと、それらのツールを使用する実践的な例を紹介します。
スキーマ識別
他のコードと同様に、スキーマは必要に応じて相互参照する論理ユニットに分割できる場合に保守が容易になります。スキーマを参照するには、スキーマを識別する方法が必要です。スキーマドキュメントは、非相対URIによって識別されます。
スキーマドキュメントに識別子を付ける必要はありませんが、あるスキーマから別のスキーマを参照する場合は識別子が必要になります。このドキュメントでは、識別子のないスキーマを「匿名スキーマ」と呼びます。
次のセクションでは、スキーマの「識別子」がどのように決定されるかを見ていきます。
URIの用語は直感的でない場合があります。このドキュメントでは、次の定義を使用します。
スキーマは 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
を使用して参照され、次のスキーマが取得されると仮定しましょう。
このスキーマのベース URI は、取得 URI である https://example.com/schemas/address
と同じです。
$id
スキーマのルートで $id
キーワードを使用することで、ベース URI を設定できます。$id
の値は、取得 URI に対して解決されるフラグメントのない URI 参照です。結果の URI がスキーマのベース URI になります。
$id
は単に id
(ドル記号なし)です。これは、HTML の <base>
タグに類似しています。
URI https://example.com/schema/address
と https://example.com/schema/billing-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
となります。
"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
は、次のスキーマで強調表示されたサブスキーマを識別します。
アンカー
サブスキーマを識別するあまり一般的でない方法は、$anchor
キーワードを使用してスキーマに名前付きアンカーを作成し、その名前をURIフラグメントで使用することです。アンカーは、先頭が文字で、その後に任意の数の文字、数字、-
、_
、:
、または.
が続く必要があります。
ドラフト4では、$id
が(ドル記号なしの)単なるid
である点を除いて、ドラフト6-7と同じ方法でアンカーを宣言します。
これらの命名規則に従わない名前付きアンカーが定義されている場合、動作は未定義です。アンカーは一部の実装では機能するかもしれませんが、他の実装では機能しない可能性があります。
URI https://example.com/schemas/address#street_address
は、次のスキーマのハイライトされた部分のサブスキーマを識別します。
$ref
スキーマは、$ref
キーワードを使用して別のスキーマを参照できます。$ref
の値は、スキーマのベース URIに対して解決されるURI参照です。$ref
を評価する際、実装は解決された識別子を使用して参照されるスキーマを取得し、そのスキーマをインスタンスに適用します。
$ref
の動作が少し異なります。オブジェクトに$ref
プロパティが含まれている場合、オブジェクトはスキーマではなく参照と見なされます。したがって、そのオブジェクトに記述した他のプロパティはJSONスキーマのキーワードとして扱われず、バリデーターによって無視されます。$ref
は、スキーマが期待される場所でのみ使用できます。この例では、顧客ごとに配送先住所と請求先住所の両方を持つ顧客レコードを定義したいとします。住所は常に同じ(番地、市区町村、都道府県)であるため、住所を保存するたびにスキーマのその部分を複製したくありません。スキーマが冗長になるだけでなく、将来の更新も困難になります。もし私たちの架空の会社が将来海外ビジネスを始め、すべての住所に国フィールドを追加したい場合、住所が使用されているすべての場所ではなく、単一の場所でこれを行う方が良いでしょう。
"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"]}
/properties/shipping_address
の $ref
は、相対的でないベース URI がなくても正常に解決できますが、/properties/billing_address
の $ref
は相対的でない URI に解決できないため、アドレススキーマを取得するために使用できません。
$defs
時には、現在のスキーマでのみ使用することを意図した小さなサブスキーマがあり、それらを個別のスキーマとして定義することは意味がありません。JSON ポインターまたは名前付きアンカーを使用して任意のサブスキーマを識別できますが、$defs
キーワードを使用すると、現在のスキーマドキュメント内で再利用することを意図したサブスキーマを保持するための標準化された場所が提供されます。
前の顧客スキーマの例を拡張して、名前プロパティに共通のスキーマを使用してみましょう。このために新しいスキーマを定義することは意味がなく、このスキーマでのみ使用されるため、$defs
を使用するのに適しています。
"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
インスタンスである場合があります。
イギリス王室の家系図の一部
上記では、自分自身を参照するスキーマを作成しました。これは、バリデーター内で事実上「ループ」を作成することになり、許可されており、有用でもあります。ただし、別の$ref
を参照する$ref
は、リゾルバー内で無限ループを引き起こす可能性があり、明示的に許可されていません。
再帰的なスキーマの拡張
ドキュメントは近日公開予定
バンドル
複数のスキーマドキュメントを扱うことは開発に便利ですが、すべてのスキーマを1つのスキーマドキュメントにバンドルする方が配布には便利なことがよくあります。これは、サブスキーマ内の$id
キーワードを使用して行うことができます。$id
がサブスキーマで使用されている場合、それは埋め込みスキーマを示します。埋め込みスキーマの識別子は、それが現れるスキーマのBase URIに対して解決された$id
の値です。埋め込みスキーマを含むスキーマドキュメントは、複合スキーマドキュメントと呼ばれます。複合スキーマドキュメント内の$id
を持つ各スキーマは、スキーマ リソースと呼ばれます。
$id
は単に id
(ドル記号なし)です。これは、HTMLの<iframe>
タグに似ています。
スキーマを開発する際に埋め込みスキーマを使用することはまれです。一般的に、この機能を明示的に使用せず、そのようなものが必要な場合はスキーマバンドルツールを使用してバンドルされたスキーマを構築するのが最適です。::
この例は、顧客スキーマの例と住所スキーマの例が複合スキーマドキュメントにバンドルされている様子を示しています。
"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
を使用して住所スキーマを参照することはできませんでした。
$id
はベースURIの変更のみを表し、埋め込みスキーマを表していなかったため、これらのURIはどちらも有効です。ただし、許可されていても、JSONポインターがベースURIが変更されたスキーマをまたがないようにすることを強くお勧めします。"$ref": "#/definitions/state"
は、埋め込みスキーマが使用されていない場合のように、トップレベルスキーマのdefinitions
キーワードではなく、住所スキーマのdefinitions
キーワードに解決されることも確認する必要があります。
各スキーマリソースは個別に評価され、異なるJSONスキーマのダイアレクトを使用する場合があります。上記の例では、住所スキーマリソースがドラフト7を使用している一方で、顧客スキーマリソースはドラフト2020-12を使用しています。埋め込みスキーマで$schema
が宣言されていない場合、デフォルトでは親スキーマのダイアレクトを使用します。
$id
はベースURIの変更に過ぎず、独立したスキーマリソースとは見なされません。$schema
はスキーマリソースのルートでのみ許可されているため、サブスキーマの$id
を使用してバンドルされたすべてのスキーマは、同じダイアレクトを使用する必要があります。$schema
を使用)のサポートが追加されました。