2024年2月15日(木) ·15分で読めます

JSON Schema における字句スコープと動的スコープの理解

JSON Schema によって定義されているほとんどの キーワードは、単独で評価することも、隣接するキーワードの値のみを考慮して評価することもできます。例えば、type キーワードは他のどのキーワードにも依存しませんが、additionalProperties キーワードは、同じスキーマオブジェクトで定義されている properties および patternProperties キーワードに依存します。

キーワードの依存関係についてさらに詳しく知りたい場合は、Greg Dennis氏による「JSON Schemaの静的解析」の記事をご覧ください。

ただし、評価がそれらが置かれている*スコープ*に依存するキーワードの小さなセットがあります。これらのキーワードは、$ref$dynamicRefunevaluatedItems、および unevaluatedPropertiesです。さらに、宣言されているスコープに*影響*を与えるキーワードのセットもあります。これらのキーワードは、$id$schema$anchor、および $dynamicAnchorです。

JSON Schema は、URI解決のために、*字句スコープ*と*動的スコープ*の2種類のスコープを定義しています。これらのスコープがどのように機能するかを理解することは、動的参照のような JSON Schema の最も高度な(そしてしばしば混乱を招く!)機能を習得するために不可欠です。

スキーマ リソース

字句スコープと動的スコープに飛び込む前に、いくつかのJSON Schemaの基本事項を確認しましょう。

$id キーワードは、スキーマのURIを定義します。このキーワードは通常トップレベルで設定されますが、任意のサブスキーマは、異なるURIで自身を区別するために宣言できます。たとえば、次のスキーマは4つの識別子を定義しており、そのうちのいくつかは相対的で、いくつかは絶対的です。

A JSON Schema with multiple identifiers

JSON Schemaの用語では、$id キーワードが新しい*スキーマ リソース*を導入し、トップレベルのスキーマ リソースが*ルート スキーマ リソース*と呼ばれます。

次の例を考えてみましょう。このスキーマは、それぞれ異なる色で強調表示された3つのスキーマ リソースで構成されています。ルート スキーマ リソース(赤)、/properties/foo のスキーマ リソース(青)、/properties/bar のスキーマ リソース(緑)です。/properties/baz のサブスキーマは、新しい識別子を導入しないため、ルート スキーマ リソースの一部であることに注意してください。

A JSON Schema that consists of 3 schema resources

子スキーマ リソースは、親スキーマ リソースの一部とは見なされないことに注意してください。たとえば、前の図では、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 から)と呼ばれるネストされたスキーマ リソースを参照しています。右側には、これらのスキーマ リソース間の関係の有向グラフ表現があります。

Thinking of a JSON Schema as a directed graph

ご覧のとおり、スキーマをスキーマ リソースの有向グラフとして考えると、字句スコープと動的スコープの両方を理解する上で非常に役立ちます。

字句スコープ

前のセクションのグラフのアナロジーでは、スキーマの字句スコープは評価対象のノードで構成されます。言い換えれば、スキーマの字句スコープは、それが属するスキーマ リソース全体で構成されます。

次の例のシーケンスを考えてみましょう。左側には、1つのネストされたスキーマ リソースを持つ JSON Schema があります。右側には、https://example.com/person という名前のルート スキーマ リソースと、https://example.com/surname という名前のネストされたスキーマ リソースに対応する有向グラフ表現があります。評価プロセスの各ステップで、スキーマと有向グラフの、字句スコープの一部ではない部分をグレーアウトしています。

評価プロセスはトップレベルのスキーマから始まります。その時点での字句スコープはルート スキーマ リソースであり、ネストされたスキーマ リソースはスコープ外です。

The lexical scope of a JSON Schema (1)

次に、properties アプリケータに入り、インスタンスが firstName プロパティを定義している場合、/properties/firstName のサブスキーマに入ります。このサブスキーマはルート スキーマ リソースの一部であるため(独自の識別子を宣言しないため)、字句スコープは前のステップと同じままです。

The lexical scope of a JSON Schema (2)

最後に、インスタンスが lastName プロパティを定義している場合、properties アプリケータに従って /properties/lastName のサブスキーマに入ります。このサブスキーマは新しいスキーマ リソースを定義するため、この時点での字句スコープはネストされたスキーマ リソースであり、ルート スキーマ リソースはスコープ外です。

The lexical scope of a JSON Schema (3)

定義上、任意のサブスキーマの字句スコープは、インスタンスを考慮することなく静的に決定できることに注意してください。まさにここで行ったように。

字句スコープとアンカー

もう1つの実用的な例として、スキーマの位置に依存しない識別子を定義する $anchor キーワードを考えてみましょう。このキーワードは、定義されているスキーマ オブジェクトだけでなく、その字句スコープにも影響を与えます。これが、同じスキーマ リソース内で同じアンカー識別子を複数回宣言するとエラーになる(字句スコープでの衝突)理由であり、異なるスキーマ リソースで同じアンカー識別子を宣言することが可能な理由です(字句スコープが異なるため)。

Example of anchors within and across lexical scopes

参照の追跡

評価プロセスが参照キーワードを検出すると、参照スキーマの字句スコープを*破棄*し、*宛先*スキーマの字句スコープに入ります。

参照先が同じスキーマ リソースのサブスキーマを指している場合、レキシカルスコープは変わりません。グラフのアナロジーに戻ると、各ノードはスキーマ リソースを表しており、評価プロセスは同じノードにとどまります。しかし、参照先が異なるスキーマ リソース上のサブスキーマを指している場合、参照先のスキーマ リソースが新しいレキシカルスコープになります。グラフのアナロジーでは、評価プロセスは別のノードに矢印をたどることになります。

スキーマ リソース内

次の例では、/items/$ref の参照は /$defs/person-name を指しています。参照先のスキーマは同じスキーマ リソース(ルート スキーマ リソース)の一部であるため、レキシカルスコープは変わりません。

Lexical scope after following a reference within the same resource

スキーマ リソースを越えて

次に、以下の例のシーケンスを検討します。左側には、ネストされたスキーマ リソース (/$defs/timestamp) と、外部スキーマ https://example.com/epoch への参照 (/anyOf/1/$ref から) を持つ https://example.com/point-in-time という名前の JSON スキーマがあります。右側には、ルート スキーマ リソース、ネストされたスキーマ リソース、および外部スキーマ リソースに対応する有向グラフ表現があります。以前と同様に、評価プロセスの各ステップで、レキシカルスコープの一部ではないスキーマと有向グラフの部分をグレーアウトします。

評価プロセスは、トップレベルのスキーマから開始されます。この時点でのレキシカルスコープはルート スキーマ リソースであり、ネストされたスキーマ リソースと外部スキーマ リソースの両方がスコープ外です。

Lexical scope after following a reference accross resources (1)

次に、anyOf 論理アプリケーターの最初のブランチに入り、/anyOf/0/$ref (赤で強調表示)の参照をたどって /$defs/timestamp に入ります。このサブスキーマには独自の識別子があるため、レキシカルスコープはネストされたスキーマ リソースになり、ルート スキーマ リソースと外部スキーマ リソースの両方がスコープ外になります。

Lexical scope after following a reference accross resources (2)

最後に、ルート スキーマ リソースに戻り、anyOf 論理アプリケーターの2番目のブランチに入り、/anyOf/1/$ref(赤で強調表示)のリモート参照をたどって https://example.com/epoch に入ります。この外部スキーマは定義上、別のスキーマ リソースです。したがって、これが新しいレキシカルスコープになります。今回は、ルート スキーマ リソースとそのネストされたスキーマ リソースの両方がスコープ外になります。

Lexical scope after following a reference accross resources (3)

ダイナミックスコープ

要約すると、スキーマのレキシカルスコープは、それを囲むスキーマ リソースで構成されます。比較すると、スキーマのダイナミックスコープは、これまでに評価されたスキーマ リソースのスタックで構成されます。スキーマをグラフに見立てたアナロジーに戻ると、ダイナミックスコープは、評価プロセスによって訪問されたノードの順序付けられたシーケンスに対応します。

次の例のシーケンスを検討してください。左上には、2つのネストされたスキーマ リソースを宣言する https://example.com/person という名前のルート スキーマ リソースがあります。https://example.com/name (/properties/name) と https://example.com/age (/properties/age)。左下には、スキーマに対して正常に検証されるインスタンスの例があります。インスタンスは、オプションの age プロパティを宣言していないことに注意してください。右側には、これらのスキーマ リソース間の関係を表す有向グラフがあります。以前に行ったのと同様に、ダイナミックスコープの一部ではないスキーマと有向グラフの部分をグレーアウトします。

評価プロセスは、トップレベルのスキーマから開始されます。この時点でのダイナミックスコープはルート スキーマ リソースであり、ネストされたスキーマ リソースはスコープ外です。これまでのところ、レキシカルスコープとダイナミックスコープは一致しています。

The dynamic scope of a JSON Schema (1)

インスタンスが name プロパティを定義しているため、/properties/name のサブスキーマに対して、properties アプリケーターに入ります。このサブスキーマは新しいスキーマ リソースを導入します。したがって、ダイナミックスコープは現在、ルート スキーマ リソースと https://example.com/name という名前のネストされたスキーマ リソース両方で構成されており、順序も守られます。

The dynamic scope of a JSON Schema (2)

レキシカルスコープと比較して、スキーマのダイナミックスコープは、評価パスがインスタンスに依存することが多いため、常に静的に決定できるとは限りません。たとえば、ifoneOf などの論理アプリケーターキーワードを利用するスキーマの場合、スコープ内のスキーマ リソースの順序付けられたシーケンスは、インスタンスの特性によって異なる場合があります。

参照の追跡

これまでのところ、レキシカルスコープの場合、参照をたどることは、参照元のスキーマのレキシカルスコープを放棄し、参照先のスキーマのレキシカルスコープに入ることであることがわかりました。比較すると、ダイナミックスコープの場合、別のスキーマ リソースへの参照をたどるには、現在のダイナミックスコープを保持し、参照先のスキーマ リソースをスタックのトップにプッシュする必要があります。

スキーマ リソース内

レキシカルスコープと同様に、参照が同じスキーマ リソース内のサブスキーマを指している場合、ダイナミックスコープは変わりません。言い換えれば、参照先のスキーマ リソースがスタックのトップにあるスキーマ リソースと同じである場合、ダイナミックスコープは変更されません。したがって、評価プロセスが(ローカルまたはリモートの)別のスキーマ リソースへの参照に遭遇するまで、レキシカルスコープとダイナミックスコープは一致します

Dynamic scope and lexical scopes sometimes align

スキーマ リソースを越えて

簡単なケースを離れて、スキーマ リソースを越えたローカルおよびリモート参照で構成される例を検討してみましょう。左上には、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 という外部スキーマ リソースの一部です。右側には、これらのスキーマ リソースとダイナミックスコープ間の関係を表す有向グラフがあります。

これまでの他の例と同様に、評価プロセスはトップレベルのスキーマから開始されます。この時点でのダイナミックスコープはルート スキーマ リソースであり、他のすべてのスキーマ リソースはスコープ外です。

The dynamic scope and remote references (1)

インスタンスが name プロパティを定義しているため、/properties/name のサブスキーマに対して、properties アプリケーターに入ります。このサブスキーマは新しいスキーマ リソースを導入します。したがって、ダイナミックスコープは現在、https://example.com (ルート スキーマ リソース) と、それに続く https://example.com/name (/properties/name のネストされたスキーマ リソース) で構成されます。

The dynamic scope and remote references (2)

https://example.com/name スキーマ リソースは、もう1つのネストされたスキーマ リソース: https://example.com/person を参照しています。この参照をたどると、ダイナミックスコープは現在、https://example.com (ルート スキーマ リソース) と、それに続く https://example.com/name (/properties/name のネストされたスキーマ リソース)、それに続く https://example.com/person (/$defs/person のネストされたスキーマ リソース) で構成されます。

The dynamic scope and remote references (3)

ここで興味深いケースが登場します。現在、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 で構成されます。

The dynamic scope and remote references (4)

スタックとしてのダイナミックスコープ

このセクションの冒頭で、スキーマのダイナミックスコープは、これまでに評価されたスキーマ リソースのスタックで構成されると述べました。ただし、これまでの例では、スキーマ リソースをスタックのトップにプッシュすることしか考慮していませんでした。

従来のプログラミング言語では、プログラムの実行には通常、他のプロシージャを呼び出すプロシージャが含まれ、コンピュータ サイエンスで「コールスタック」と呼ばれるものが作成されます。最終的に、プロシージャは他のプロシージャを呼び出しません。そのようなリーフプロシージャの実行が終了すると、コールスタックは巻き戻し (ポップ操作) され、制御が呼び出し元のフレームに戻ります。

前の段落が理解できない場合は、ハーバード大学の コールスタック - CS50 Shorts をご覧ください。

JSON スキーマのダイナミックスコープも同じように動作します。ある時点で、スキーマ リソースは他のスキーマ リソースを参照しなくなります。次に、ダイナミックスコープが巻き戻され、スタックから最後のスキーマ リソースがポップされます。

以下の例のシーケンスを考えてみましょう。左上には、https://example.com/integer という名前のルートスキーマのリソースがあり、ifthen、およびelseのロジックアプライケーターを使用して、正の整数が偶数か奇数かをチェックし、対応するtitleアノテーションを生成します。各サブスキーマは、個別のスキーマのリソースであることに注意してください。https://example.com/check/if)、https://example.com/even/then)、およびhttps://example.com/odd/else)。左下には、偶数の整数インスタンス42があります。右側には、これらのスキーマのリソースと動的スコープの関係を表す有向グラフがあります。

通常どおり、評価プロセスはトップレベルのスキーマから開始されます。この時点での動的スコープはルートスキーマのリソースであり、他のすべてのスキーマのリソースはスコープ外です。

The dynamic scope as a stack (1)

次に、整数インスタンスが偶数か奇数かをチェックするifアプライケーターに入ります。このサブスキーマは、https://example.com/checkという名前の新しいスキーマのリソースを宣言し、これがスタックにプッシュされます。したがって、動的スコープは、https://example.com/integerの後にhttps://example.com/checkが続くことになります。

The dynamic scope as a stack (2)

https://example.com/checkというネストされたスキーマのリソースは、他のスキーマのリソースを参照していません。評価プロセスが完了して、インスタンスが偶数整数であると判断すると、スタックが巻き戻され、https://example.com/checkスキーマのリソースがポップされ、評価プロセスはルートスキーマのリソースに戻ります。したがって、動的スコープはhttps://example.com/integerのみに戻ります。

The dynamic scope as a stack (3)

ifサブスキーマがインスタンスを正常に検証したため、thenアプライケーターに入ります。このサブスキーマは、https://example.com/evenという名前の新しいスキーマのリソースを宣言し、これがスタックにプッシュされます。したがって、動的スコープは、https://example.com/integerの後にhttps://example.com/evenが続くことになります。

The dynamic scope as a stack (4)

以前と同様に、https://example.com/evenというネストされたスキーマのリソースは、他のスキーマのリソースを参照していません。したがって、評価プロセスは再びルートスキーマのリソースに戻り、動的スコープはhttps://example.com/integerのみに戻り、評価プロセスが完了します。

The dynamic scope as a stack (5)

要約

静的スコープと動的スコープがどのように機能するかを理解することは、JSONスキーマをより深く理解するために不可欠です。覚えておくべき最も重要な点を以下の表にまとめます。

比較ポイント字句スコープダイナミックスコープ
定義評価されているスキーマのリソースで構成されますこれまでに評価されたスキーマのリソースのスタックで構成されます
スコープの決定インスタンスを考慮に入れなくても静的に決定できます常に静的に決定できるとは限りません。インスタンスによって異なる場合があります
参照の追跡元のスキーマの字句スコープを放棄し、宛先スキーマの字句スコープに入ることで構成されます宛先スキーマのリソースを動的スコープスタックのトップにプッシュすることで構成されます

今後の投稿では、この記事で紹介した概念に基づいて、動的参照($dynamicRef$dynamicAnchor)がどのように機能するかを解明します。

このコンテンツを楽しんでいただき、データ業界でJSONスキーマのスキルを実践したい場合は、私のO'Reillyの本をご覧ください。Unifying Business, Data, and Code: Designing Data Products using JSON Schema。また、LinkedInで私とつながることもできます。

Image by Christina Morillo from Pexels.