2024 年 2 月 15 日 星期四 ·15分鐘閱讀

了解 JSON Schema 的詞彙和動態範圍

JSON Schema 組織定義的大多數關鍵字可以單獨評估,或者僅考慮其相鄰關鍵字的值進行評估。例如,type 關鍵字與任何其他關鍵字無關,而 additionalProperties 關鍵字則取決於在同一個 schema 物件中定義的 propertiespatternProperties 關鍵字。

如果您想了解更多關於關鍵字依賴性的資訊,請查看 JSON Schema 的靜態分析這篇文章,作者是 Greg Dennis

然而,有一小部分關鍵字的評估取決於它們所在的範圍。這些關鍵字是 $ref$dynamicRefunevaluatedItemsunevaluatedProperties。此外,還有一組關鍵字會影響它們被宣告的範圍。這些關鍵字是 $id$schema$anchor$dynamicAnchor

JSON Schema 定義了兩種範圍類型,用於 URI 解析:詞彙範圍動態範圍。了解這些範圍如何運作對於掌握 JSON Schema 中一些最先進(且經常令人困惑!)的功能至關重要,例如動態參照。

Schema 資源

在我們深入探討詞彙和動態範圍之前,讓我們先複習一些 JSON Schema 的基本概念。

$id 關鍵字定義了schema 的 URI。雖然此關鍵字通常設定在頂層,但任何子 schema 都可以宣告它,以便用不同的 URI 來區分自己。例如,以下 schema 定義了 4 個識別符,其中一些是相對的,另一些是絕對的。

A JSON Schema with multiple identifiers

在 JSON Schema 的術語中,我們說 $id 關鍵字引入了一個新的schema 資源,而頂層 schema 資源被稱為根 schema 資源

請看以下範例。此 schema 由 3 個 schema 資源組成,每個資源使用不同的顏色突出顯示:根 schema 資源(紅色)、位於 /properties/foo 的 schema 資源(藍色)和位於 /properties/bar 的 schema 資源(綠色)。請注意,位於 /properties/baz 的子 schema 是根 schema 資源的一部分,因為它沒有引入新的識別符。

A JSON Schema that consists of 3 schema resources

請注意,子 schema 資源不被視為父 schema 資源的一部分。例如,在上圖中,https://example.com/foohttps://example.com/bar獨立的 schema 資源,而不是根 schema 資源的一部分,儘管它們之間存在結構關係。

Schema 作為有向圖

JSON Schema 是一種遞迴資料結構。在 schema 資源的上下文中,這表示 schema 資源可能會引入巢狀的 schema 資源(如我們在上一節所看到的),並使用參照關鍵字(如 $ref)指向外部 schema 資源,從而建立一個 schema 資源的有向圖。

請看以下範例。在左上方,一個名為 https://example.com/origin 的根 schema 資源,它宣告了一個名為 https://example.com/nested 的巢狀 schema 資源(位於 /properties/bar),並參照了一個名為 https://example.com/destination 的外部 schema 資源(來自 /properties/foo/$ref)。在左下方,一個名為 https://example.com/destination 的根 schema 資源,它參照了一個名為 https://example.com/nested-string 的巢狀 schema 資源(來自 /items/$ref)。在右側,這些 schema 資源之間的關係的有向圖表示。

Thinking of a JSON Schema as a directed graph

您將會看到,將 schema 視為 schema 資源的有向圖,對於理解詞彙和動態範圍非常有幫助。

詞彙範圍

在上一節的圖形類比中,schema 的詞彙範圍由正在評估的節點組成。換句話說,schema 的詞彙範圍由它所屬的整個 schema 資源組成。

請看以下範例序列。在左側,一個 JSON Schema 具有單個巢狀 schema 資源。在右側,名為 https://example.com/person 的根 schema 資源和名為 https://example.com/surname 的巢狀 schema 資源的對應有向圖表示。在評估過程的每個步驟中,我們會將 schema 和有向圖中不屬於詞彙範圍的部分灰色顯示。

評估過程從頂層 schema 開始。此時的詞彙範圍是根 schema 資源,巢狀 schema 資源超出範圍。

The lexical scope of a JSON Schema (1)

然後,我們進入 properties 應用程式,如果實例定義了 firstName 屬性,我們會進入位於 /properties/firstName 的子 schema。此子 schema 是根 schema 資源的一部分(因為它沒有宣告自己的識別符),因此詞彙範圍與上一步相同。

The lexical scope of a JSON Schema (2)

最後,如果實例定義了 lastName 屬性,我們會依照 properties 應用程式進入位於 /properties/lastName 的子 schema。此子 schema 定義了一個新的 schema 資源,因此此時的詞彙範圍是巢狀 schema 資源,而根 schema 資源超出範圍。

The lexical scope of a JSON Schema (3)

請注意,根據定義,任何子 schema 的詞彙範圍都可以靜態地確定,而無需考慮實例,就像我們在這裡所做的那樣。

詞彙範圍和錨點

作為另一個實際範例,請考慮 $anchor 關鍵字,它為 schema 定義了一個獨立於位置的識別符。此關鍵字不僅會影響它定義的 schema 物件,還會影響其詞彙範圍。這就是為什麼在同一個 schema 資源中多次宣告相同的錨點識別符會發生錯誤(詞彙範圍衝突),而可以在不同的 schema 資源上宣告相同的錨點識別符(因為詞彙範圍不同)。

Example of anchors within and across lexical scopes

追蹤參照

當評估過程遇到參照關鍵字時,它會放棄參照 schema 的詞彙範圍,並進入目的地 schema 的詞彙範圍。

如果參照指向同一個 schema 資源內的子 schema,則詞彙範圍保持不變。回到圖形類比,每個節點代表一個 schema 資源,因此評估過程仍停留在同一個節點上。但是,如果參照指向不同 schema 資源上的子 schema,則目的地的 schema 資源會成為新的詞彙範圍。在圖形類比中,評估過程會跟隨箭頭到另一個節點。

在 Schema 資源內

在以下範例中,/items/$ref 的參照指向 /$defs/person-name。目標綱要屬於同一個綱要資源(根綱要資源),因此詞彙範圍保持不變。

Lexical scope after following a reference within the same resource

跨綱要資源

現在考慮以下範例序列。左側是一個名為 https://example.com/point-in-time 的 JSON 綱要,其中包含一個巢狀綱要資源(位於 /$defs/timestamp)和一個對名為 https://example.com/epoch 的外部綱要的參照(來自 /anyOf/1/$ref)。右側是根綱要資源、巢狀綱要資源和外部綱要資源的對應有向圖表示。與之前一樣,在評估過程的每個步驟中,我們會將不屬於詞彙範圍的綱要和有向圖部分以灰色顯示。

評估過程從頂層綱要開始。此時的詞彙範圍是根綱要資源,而巢狀綱要資源和外部綱要資源都超出範圍。

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 邏輯運算子的第二個分支,並追蹤 /anyOf/1/$ref(以紅色標示)的遠端參照到 https://example.com/epoch。這個外部綱要根據定義是一個獨立的綱要資源。因此,它會成為新的詞彙範圍。這次,根綱要資源及其巢狀綱要資源都超出範圍。

Lexical scope after following a reference accross resources (3)

動態範圍

總而言之,綱要的詞彙範圍由其封閉的綱要資源組成。相比之下,綱要的動態範圍由目前已評估的綱要資源堆疊組成。回到我們將綱要比喻為圖形的說法,動態範圍對應於評估過程所訪問的節點的有序序列。

考慮以下範例序列。在左上方,一個名為 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 運算子,進入位於 /properties/name 的子綱要。這個子綱要引入了一個新的綱要資源。因此,動態範圍現在由兩個部分組成,依序為根綱要資源和名為 https://example.com/name 的巢狀綱要資源。

The dynamic scope of a JSON Schema (2)

與詞彙範圍相比,綱要的動態範圍並非總是可以靜態地確定,因為評估路徑通常取決於實例。例如,對於使用諸如 ifoneOf 之類的邏輯運算子關鍵字的綱要,範圍內的綱要資源有序序列可能會因實例的特性而異。

追蹤參照

到目前為止,我們已了解,對於詞彙範圍,追蹤參照包括放棄來源綱要的詞彙範圍,並進入目標綱要的詞彙範圍。相比之下,對於動態範圍,追蹤另一個綱要資源的參照涉及保留目前的動態範圍,並將目標綱要資源推入堆疊頂端。

在 Schema 資源內

與詞彙範圍一樣,如果參照指向同一個綱要資源中的子綱要,則動態範圍保持不變。換句話說,如果目標綱要資源與堆疊頂端的綱要資源相同,則動態範圍不會被修改。因此,在評估過程遇到對另一個綱要資源(無論是本機或遠端)的參照之前,*詞彙範圍和動態範圍會對齊*。

Dynamic scope and lexical scopes sometimes align

跨綱要資源

將簡單情況拋在一旁,讓我們考慮一個包含跨綱要資源的本機和遠端參照的範例。在左上方,一個範例實例和一個名為 https://example.com 的根綱要資源,其中宣告了兩個巢狀綱要資源:https://example.com/name(位於 /properties/name)和 https://example.com/person(位於 /$defs/person),前者參照後者(來自 /properties/name/$ref)。此外,https://example.com/person 參照一個名為 item 的錨定綱要(來自 /$defs/person/$ref),該綱要屬於一個名為 https://example.com/people 的外部綱要資源,如左下方所示。在右側,是有向圖,表示這些綱要資源和動態範圍之間的關係。

與到目前為止的其他範例一樣,評估過程從頂層綱要開始。此時的動態範圍是根綱要資源,所有其他綱要資源都超出範圍。

The dynamic scope and remote references (1)

由於該實例定義了 name 屬性,因此我們進入 properties 運算子,進入位於 /properties/name 的子綱要。這個子綱要引入了一個新的綱要資源。因此,動態範圍現在由 https://example.com(根綱要資源)組成,後接 https://example.com/name(位於 /properties/name 的巢狀綱要資源)。

The dynamic scope and remote references (2)

https://example.com/name 綱要資源參照另一個巢狀綱要資源: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 的根綱要資源,它使用 ifthenelse 邏輯應用器來檢查正整數是偶數還是奇數,並產生相應的 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 Schema 至關重要。以下表格總結了最重要的重點:

比較點詞彙範圍動態範圍
定義由正在評估的綱要資源組成由目前為止已評估的綱要資源堆疊組成
決定作用域可以靜態決定,無需考慮實例無法總是靜態決定。它可能會根據實例而變化
追蹤參考包括放棄來源綱要的詞法作用域,並進入目標綱要的詞法作用域包括將目標綱要資源推送到動態作用域堆疊的頂部

在未來的文章中,我們將建立在本文介紹的概念之上,以解開動態參考($dynamicRef$dynamicAnchor)的工作原理。

如果您喜歡這篇文章並想將您的 JSON Schema 技能應用於資料產業,請查看我的 O'Reilly 書籍:Unifying Business, Data, and Code: Designing Data Products using JSON Schema。您也可以在 LinkedIn 上與我聯繫。

圖片由 Christina Morillo 來自 Pexels