JSON Schema における字句スコープと動的スコープの理解
JSON Schema によって定義されているほとんどの キーワードは、単独で評価することも、隣接するキーワードの値のみを考慮して評価することもできます。例えば、type
キーワードは他のどのキーワードにも依存しませんが、additionalProperties
キーワードは、同じスキーマオブジェクトで定義されている properties
および patternProperties
キーワードに依存します。
キーワードの依存関係についてさらに詳しく知りたい場合は、Greg Dennis氏による「JSON Schemaの静的解析」の記事をご覧ください。
ただし、評価がそれらが置かれている*スコープ*に依存するキーワードの小さなセットがあります。これらのキーワードは、$ref
、$dynamicRef
、unevaluatedItems
、および unevaluatedProperties
です。さらに、宣言されているスコープに*影響*を与えるキーワードのセットもあります。これらのキーワードは、$id
、$schema
、$anchor
、および $dynamicAnchor
です。
JSON Schema は、URI解決のために、*字句スコープ*と*動的スコープ*の2種類のスコープを定義しています。これらのスコープがどのように機能するかを理解することは、動的参照のような JSON Schema の最も高度な(そしてしばしば混乱を招く!)機能を習得するために不可欠です。
スキーマ リソース
字句スコープと動的スコープに飛び込む前に、いくつかのJSON Schemaの基本事項を確認しましょう。
$id
キーワードは、スキーマのURIを定義します。このキーワードは通常トップレベルで設定されますが、任意のサブスキーマは、異なるURIで自身を区別するために宣言できます。たとえば、次のスキーマは4つの識別子を定義しており、そのうちのいくつかは相対的で、いくつかは絶対的です。
JSON Schemaの用語では、$id
キーワードが新しい*スキーマ リソース*を導入し、トップレベルのスキーマ リソースが*ルート スキーマ リソース*と呼ばれます。
次の例を考えてみましょう。このスキーマは、それぞれ異なる色で強調表示された3つのスキーマ リソースで構成されています。ルート スキーマ リソース(赤)、/properties/foo
のスキーマ リソース(青)、/properties/bar
のスキーマ リソース(緑)です。/properties/baz
のサブスキーマは、新しい識別子を導入しないため、ルート スキーマ リソースの一部であることに注意してください。
子スキーマ リソースは、親スキーマ リソースの一部とは見なされないことに注意してください。たとえば、前の図では、https://example.com/foo
または https://example.com/bar
は、構造的な関係にもかかわらず、ルート スキーマ リソースの一部ではなく、*別個の*スキーマ リソースです。
有向グラフとしてのスキーマ
JSON Schemaは再帰的なデータ構造です。スキーマ リソースのコンテキストでは、これはスキーマ リソースがネストされたスキーマ リソースを導入し(前のセクションで見たように)、参照キーワード($ref
など)を使用して外部スキーマ リソースを指し、スキーマ リソースの有向グラフを作成できることを意味します。
次の例を考えてみましょう。左上には、https://example.com/origin
という名前のルート スキーマ リソースがあり、https://example.com/nested
(/properties/bar
)という名前のネストされたスキーマ リソースを宣言し、https://example.com/destination
(/properties/foo/$ref
から)という名前の外部スキーマ リソースを参照しています。左下には、https://example.com/destination
という名前のルート スキーマ リソースがあり、https://example.com/nested-string
(/items/$ref
から)と呼ばれるネストされたスキーマ リソースを参照しています。右側には、これらのスキーマ リソース間の関係の有向グラフ表現があります。
ご覧のとおり、スキーマをスキーマ リソースの有向グラフとして考えると、字句スコープと動的スコープの両方を理解する上で非常に役立ちます。
字句スコープ
前のセクションのグラフのアナロジーでは、スキーマの字句スコープは評価対象のノードで構成されます。言い換えれば、スキーマの字句スコープは、それが属するスキーマ リソース全体で構成されます。
次の例のシーケンスを考えてみましょう。左側には、1つのネストされたスキーマ リソースを持つ JSON Schema があります。右側には、https://example.com/person
という名前のルート スキーマ リソースと、https://example.com/surname
という名前のネストされたスキーマ リソースに対応する有向グラフ表現があります。評価プロセスの各ステップで、スキーマと有向グラフの、字句スコープの一部ではない部分をグレーアウトしています。
評価プロセスはトップレベルのスキーマから始まります。その時点での字句スコープはルート スキーマ リソースであり、ネストされたスキーマ リソースはスコープ外です。
次に、properties
アプリケータに入り、インスタンスが firstName
プロパティを定義している場合、/properties/firstName
のサブスキーマに入ります。このサブスキーマはルート スキーマ リソースの一部であるため(独自の識別子を宣言しないため)、字句スコープは前のステップと同じままです。
最後に、インスタンスが lastName
プロパティを定義している場合、properties
アプリケータに従って /properties/lastName
のサブスキーマに入ります。このサブスキーマは新しいスキーマ リソースを定義するため、この時点での字句スコープはネストされたスキーマ リソースであり、ルート スキーマ リソースはスコープ外です。
定義上、任意のサブスキーマの字句スコープは、インスタンスを考慮することなく静的に決定できることに注意してください。まさにここで行ったように。
字句スコープとアンカー
もう1つの実用的な例として、スキーマの位置に依存しない識別子を定義する $anchor
キーワードを考えてみましょう。このキーワードは、定義されているスキーマ オブジェクトだけでなく、その字句スコープにも影響を与えます。これが、同じスキーマ リソース内で同じアンカー識別子を複数回宣言するとエラーになる(字句スコープでの衝突)理由であり、異なるスキーマ リソースで同じアンカー識別子を宣言することが可能な理由です(字句スコープが異なるため)。
参照の追跡
評価プロセスが参照キーワードを検出すると、参照スキーマの字句スコープを*破棄*し、*宛先*スキーマの字句スコープに入ります。
参照先が同じスキーマ リソース内のサブスキーマを指している場合、レキシカルスコープは変わりません。グラフのアナロジーに戻ると、各ノードはスキーマ リソースを表しており、評価プロセスは同じノードにとどまります。しかし、参照先が異なるスキーマ リソース上のサブスキーマを指している場合、参照先のスキーマ リソースが新しいレキシカルスコープになります。グラフのアナロジーでは、評価プロセスは別のノードに矢印をたどることになります。
スキーマ リソース内
次の例では、/items/$ref
の参照は /$defs/person-name
を指しています。参照先のスキーマは同じスキーマ リソース(ルート スキーマ リソース)の一部であるため、レキシカルスコープは変わりません。
スキーマ リソースを越えて
次に、以下の例のシーケンスを検討します。左側には、ネストされたスキーマ リソース (/$defs/timestamp
) と、外部スキーマ https://example.com/epoch
への参照 (/anyOf/1/$ref
から) を持つ https://example.com/point-in-time
という名前の JSON スキーマがあります。右側には、ルート スキーマ リソース、ネストされたスキーマ リソース、および外部スキーマ リソースに対応する有向グラフ表現があります。以前と同様に、評価プロセスの各ステップで、レキシカルスコープの一部ではないスキーマと有向グラフの部分をグレーアウトします。
評価プロセスは、トップレベルのスキーマから開始されます。この時点でのレキシカルスコープはルート スキーマ リソースであり、ネストされたスキーマ リソースと外部スキーマ リソースの両方がスコープ外です。
次に、anyOf
論理アプリケーターの最初のブランチに入り、/anyOf/0/$ref
(赤で強調表示)の参照をたどって /$defs/timestamp
に入ります。このサブスキーマには独自の識別子があるため、レキシカルスコープはネストされたスキーマ リソースになり、ルート スキーマ リソースと外部スキーマ リソースの両方がスコープ外になります。
最後に、ルート スキーマ リソースに戻り、anyOf
論理アプリケーターの2番目のブランチに入り、/anyOf/1/$ref
(赤で強調表示)のリモート参照をたどって https://example.com/epoch
に入ります。この外部スキーマは定義上、別のスキーマ リソースです。したがって、これが新しいレキシカルスコープになります。今回は、ルート スキーマ リソースとそのネストされたスキーマ リソースの両方がスコープ外になります。
ダイナミックスコープ
要約すると、スキーマのレキシカルスコープは、それを囲むスキーマ リソースで構成されます。比較すると、スキーマのダイナミックスコープは、これまでに評価されたスキーマ リソースのスタックで構成されます。スキーマをグラフに見立てたアナロジーに戻ると、ダイナミックスコープは、評価プロセスによって訪問されたノードの順序付けられたシーケンスに対応します。
次の例のシーケンスを検討してください。左上には、2つのネストされたスキーマ リソースを宣言する https://example.com/person
という名前のルート スキーマ リソースがあります。https://example.com/name
(/properties/name
) と https://example.com/age
(/properties/age
)。左下には、スキーマに対して正常に検証されるインスタンスの例があります。インスタンスは、オプションの age
プロパティを宣言していないことに注意してください。右側には、これらのスキーマ リソース間の関係を表す有向グラフがあります。以前に行ったのと同様に、ダイナミックスコープの一部ではないスキーマと有向グラフの部分をグレーアウトします。
評価プロセスは、トップレベルのスキーマから開始されます。この時点でのダイナミックスコープはルート スキーマ リソースであり、ネストされたスキーマ リソースはスコープ外です。これまでのところ、レキシカルスコープとダイナミックスコープは一致しています。
インスタンスが name
プロパティを定義しているため、/properties/name
のサブスキーマに対して、properties
アプリケーターに入ります。このサブスキーマは新しいスキーマ リソースを導入します。したがって、ダイナミックスコープは現在、ルート スキーマ リソースと https://example.com/name
という名前のネストされたスキーマ リソース両方で構成されており、順序も守られます。
レキシカルスコープと比較して、スキーマのダイナミックスコープは、評価パスがインスタンスに依存することが多いため、常に静的に決定できるとは限りません。たとえば、if
や oneOf
などの論理アプリケーターキーワードを利用するスキーマの場合、スコープ内のスキーマ リソースの順序付けられたシーケンスは、インスタンスの特性によって異なる場合があります。
参照の追跡
これまでのところ、レキシカルスコープの場合、参照をたどることは、参照元のスキーマのレキシカルスコープを放棄し、参照先のスキーマのレキシカルスコープに入ることであることがわかりました。比較すると、ダイナミックスコープの場合、別のスキーマ リソースへの参照をたどるには、現在のダイナミックスコープを保持し、参照先のスキーマ リソースをスタックのトップにプッシュする必要があります。
スキーマ リソース内
レキシカルスコープと同様に、参照が同じスキーマ リソース内のサブスキーマを指している場合、ダイナミックスコープは変わりません。言い換えれば、参照先のスキーマ リソースがスタックのトップにあるスキーマ リソースと同じである場合、ダイナミックスコープは変更されません。したがって、評価プロセスが(ローカルまたはリモートの)別のスキーマ リソースへの参照に遭遇するまで、レキシカルスコープとダイナミックスコープは一致します。
スキーマ リソースを越えて
簡単なケースを離れて、スキーマ リソースを越えたローカルおよびリモート参照で構成される例を検討してみましょう。左上には、https://example.com
という名前のルート スキーマ リソースと、2つのネストされたスキーマ リソースを宣言するインスタンスの例があります。https://example.com/name
(/properties/name
) と、後者が前者 (/properties/name/$ref
から) を参照する https://example.com/person
(/$defs/person
)。さらに、https://example.com/person
は、item
という名前のアンカー付きスキーマ (/$defs/person/$ref
から) を参照しています。これは、左下に示す https://example.com/people
という外部スキーマ リソースの一部です。右側には、これらのスキーマ リソースとダイナミックスコープ間の関係を表す有向グラフがあります。
これまでの他の例と同様に、評価プロセスはトップレベルのスキーマから開始されます。この時点でのダイナミックスコープはルート スキーマ リソースであり、他のすべてのスキーマ リソースはスコープ外です。
インスタンスが name
プロパティを定義しているため、/properties/name
のサブスキーマに対して、properties
アプリケーターに入ります。このサブスキーマは新しいスキーマ リソースを導入します。したがって、ダイナミックスコープは現在、https://example.com
(ルート スキーマ リソース) と、それに続く https://example.com/name
(/properties/name
のネストされたスキーマ リソース) で構成されます。
https://example.com/name
スキーマ リソースは、もう1つのネストされたスキーマ リソース: https://example.com/person
を参照しています。この参照をたどると、ダイナミックスコープは現在、https://example.com
(ルート スキーマ リソース) と、それに続く https://example.com/name
(/properties/name
のネストされたスキーマ リソース)、それに続く https://example.com/person
(/$defs/person
のネストされたスキーマ リソース) で構成されます。
ここで興味深いケースが登場します。現在、https://example.com/person
という名前のネストされたスキーマ リソースを評価しています。このスキーマ リソースは、https://example.com/people
というリモート スキーマ (people#item
URI 参照の people
の部分) を指していますが、そのルートには着地しません。代わりに、/items
のサブスキーマ(people#item
URI 参照の item
アンカーが配置されている場所)に着地します。このサブスキーマはルート スキーマ リソースの一部であるため、ダイナミックスコープは現在、https://example.com
(ルート スキーマ リソース) と、それに続く https://example.com/name
(/properties/name
のネストされたスキーマ リソース)、それに続く https://example.com/person
(/$defs/person
のネストされたスキーマ リソース)、それに続く https://example.com/people
で構成されます。
スタックとしてのダイナミックスコープ
このセクションの冒頭で、スキーマのダイナミックスコープは、これまでに評価されたスキーマ リソースのスタックで構成されると述べました。ただし、これまでの例では、スキーマ リソースをスタックのトップにプッシュすることしか考慮していませんでした。
従来のプログラミング言語では、プログラムの実行には通常、他のプロシージャを呼び出すプロシージャが含まれ、コンピュータ サイエンスで「コールスタック」と呼ばれるものが作成されます。最終的に、プロシージャは他のプロシージャを呼び出しません。そのようなリーフプロシージャの実行が終了すると、コールスタックは巻き戻し (ポップ操作) され、制御が呼び出し元のフレームに戻ります。
前の段落が理解できない場合は、ハーバード大学の コールスタック - CS50 Shorts をご覧ください。
JSON スキーマのダイナミックスコープも同じように動作します。ある時点で、スキーマ リソースは他のスキーマ リソースを参照しなくなります。次に、ダイナミックスコープが巻き戻され、スタックから最後のスキーマ リソースがポップされます。
以下の例のシーケンスを考えてみましょう。左上には、https://example.com/integer
という名前のルートスキーマのリソースがあり、if
、then
、およびelse
のロジックアプライケーターを使用して、正の整数が偶数か奇数かをチェックし、対応するtitle
アノテーションを生成します。各サブスキーマは、個別のスキーマのリソースであることに注意してください。https://example.com/check
(/if
)、https://example.com/even
(/then
)、およびhttps://example.com/odd
(/else
)。左下には、偶数の整数インスタンス42があります。右側には、これらのスキーマのリソースと動的スコープの関係を表す有向グラフがあります。
通常どおり、評価プロセスはトップレベルのスキーマから開始されます。この時点での動的スコープはルートスキーマのリソースであり、他のすべてのスキーマのリソースはスコープ外です。
次に、整数インスタンスが偶数か奇数かをチェックするif
アプライケーターに入ります。このサブスキーマは、https://example.com/check
という名前の新しいスキーマのリソースを宣言し、これがスタックにプッシュされます。したがって、動的スコープは、https://example.com/integer
の後にhttps://example.com/check
が続くことになります。
https://example.com/check
というネストされたスキーマのリソースは、他のスキーマのリソースを参照していません。評価プロセスが完了して、インスタンスが偶数整数であると判断すると、スタックが巻き戻され、https://example.com/check
スキーマのリソースがポップされ、評価プロセスはルートスキーマのリソースに戻ります。したがって、動的スコープはhttps://example.com/integer
のみに戻ります。
if
サブスキーマがインスタンスを正常に検証したため、then
アプライケーターに入ります。このサブスキーマは、https://example.com/even
という名前の新しいスキーマのリソースを宣言し、これがスタックにプッシュされます。したがって、動的スコープは、https://example.com/integer
の後にhttps://example.com/even
が続くことになります。
以前と同様に、https://example.com/even
というネストされたスキーマのリソースは、他のスキーマのリソースを参照していません。したがって、評価プロセスは再びルートスキーマのリソースに戻り、動的スコープはhttps://example.com/integer
のみに戻り、評価プロセスが完了します。
要約
静的スコープと動的スコープがどのように機能するかを理解することは、JSONスキーマをより深く理解するために不可欠です。覚えておくべき最も重要な点を以下の表にまとめます。
比較ポイント | 字句スコープ | ダイナミックスコープ |
---|---|---|
定義 | 評価されているスキーマのリソースで構成されます | これまでに評価されたスキーマのリソースのスタックで構成されます |
スコープの決定 | インスタンスを考慮に入れなくても静的に決定できます | 常に静的に決定できるとは限りません。インスタンスによって異なる場合があります |
参照の追跡 | 元のスキーマの字句スコープを放棄し、宛先スキーマの字句スコープに入ることで構成されます | 宛先スキーマのリソースを動的スコープスタックのトップにプッシュすることで構成されます |
今後の投稿では、この記事で紹介した概念に基づいて、動的参照($dynamicRef
と$dynamicAnchor
)がどのように機能するかを解明します。
このコンテンツを楽しんでいただき、データ業界でJSONスキーマのスキルを実践したい場合は、私のO'Reillyの本をご覧ください。Unifying Business, Data, and Code: Designing Data Products using JSON Schema。また、LinkedInで私とつながることもできます。
Image by Christina Morillo from Pexels.