了解 JSON Schema 的詞彙和動態範圍
JSON Schema 組織定義的大多數關鍵字可以單獨評估,或者僅考慮其相鄰關鍵字的值進行評估。例如,type
關鍵字與任何其他關鍵字無關,而 additionalProperties
關鍵字則取決於在同一個 schema 物件中定義的 properties
和 patternProperties
關鍵字。
如果您想了解更多關於關鍵字依賴性的資訊,請查看 JSON Schema 的靜態分析這篇文章,作者是 Greg Dennis。
然而,有一小部分關鍵字的評估取決於它們所在的範圍。這些關鍵字是 $ref
、$dynamicRef
、unevaluatedItems
和 unevaluatedProperties
。此外,還有一組關鍵字會影響它們被宣告的範圍。這些關鍵字是 $id
、$schema
、$anchor
和 $dynamicAnchor
。
JSON Schema 定義了兩種範圍類型,用於 URI 解析:詞彙範圍和動態範圍。了解這些範圍如何運作對於掌握 JSON Schema 中一些最先進(且經常令人困惑!)的功能至關重要,例如動態參照。
Schema 資源
在我們深入探討詞彙和動態範圍之前,讓我們先複習一些 JSON Schema 的基本概念。
$id
關鍵字定義了schema 的 URI。雖然此關鍵字通常設定在頂層,但任何子 schema 都可以宣告它,以便用不同的 URI 來區分自己。例如,以下 schema 定義了 4 個識別符,其中一些是相對的,另一些是絕對的。
在 JSON Schema 的術語中,我們說 $id
關鍵字引入了一個新的schema 資源,而頂層 schema 資源被稱為根 schema 資源。
請看以下範例。此 schema 由 3 個 schema 資源組成,每個資源使用不同的顏色突出顯示:根 schema 資源(紅色)、位於 /properties/foo
的 schema 資源(藍色)和位於 /properties/bar
的 schema 資源(綠色)。請注意,位於 /properties/baz
的子 schema 是根 schema 資源的一部分,因為它沒有引入新的識別符。
請注意,子 schema 資源不被視為父 schema 資源的一部分。例如,在上圖中,https://example.com/foo
或 https://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 資源之間的關係的有向圖表示。
您將會看到,將 schema 視為 schema 資源的有向圖,對於理解詞彙和動態範圍非常有幫助。
詞彙範圍
在上一節的圖形類比中,schema 的詞彙範圍由正在評估的節點組成。換句話說,schema 的詞彙範圍由它所屬的整個 schema 資源組成。
請看以下範例序列。在左側,一個 JSON Schema 具有單個巢狀 schema 資源。在右側,名為 https://example.com/person
的根 schema 資源和名為 https://example.com/surname
的巢狀 schema 資源的對應有向圖表示。在評估過程的每個步驟中,我們會將 schema 和有向圖中不屬於詞彙範圍的部分灰色顯示。
評估過程從頂層 schema 開始。此時的詞彙範圍是根 schema 資源,巢狀 schema 資源超出範圍。
然後,我們進入 properties
應用程式,如果實例定義了 firstName
屬性,我們會進入位於 /properties/firstName
的子 schema。此子 schema 是根 schema 資源的一部分(因為它沒有宣告自己的識別符),因此詞彙範圍與上一步相同。
最後,如果實例定義了 lastName
屬性,我們會依照 properties
應用程式進入位於 /properties/lastName
的子 schema。此子 schema 定義了一個新的 schema 資源,因此此時的詞彙範圍是巢狀 schema 資源,而根 schema 資源超出範圍。
請注意,根據定義,任何子 schema 的詞彙範圍都可以靜態地確定,而無需考慮實例,就像我們在這裡所做的那樣。
詞彙範圍和錨點
作為另一個實際範例,請考慮 $anchor
關鍵字,它為 schema 定義了一個獨立於位置的識別符。此關鍵字不僅會影響它定義的 schema 物件,還會影響其詞彙範圍。這就是為什麼在同一個 schema 資源中多次宣告相同的錨點識別符會發生錯誤(詞彙範圍衝突),而可以在不同的 schema 資源上宣告相同的錨點識別符(因為詞彙範圍不同)。
追蹤參照
當評估過程遇到參照關鍵字時,它會放棄參照 schema 的詞彙範圍,並進入目的地 schema 的詞彙範圍。
如果參照指向同一個 schema 資源內的子 schema,則詞彙範圍保持不變。回到圖形類比,每個節點代表一個 schema 資源,因此評估過程仍停留在同一個節點上。但是,如果參照指向不同 schema 資源上的子 schema,則目的地的 schema 資源會成為新的詞彙範圍。在圖形類比中,評估過程會跟隨箭頭到另一個節點。
在 Schema 資源內
在以下範例中,/items/$ref
的參照指向 /$defs/person-name
。目標綱要屬於同一個綱要資源(根綱要資源),因此詞彙範圍保持不變。
跨綱要資源
現在考慮以下範例序列。左側是一個名為 https://example.com/point-in-time
的 JSON 綱要,其中包含一個巢狀綱要資源(位於 /$defs/timestamp
)和一個對名為 https://example.com/epoch
的外部綱要的參照(來自 /anyOf/1/$ref
)。右側是根綱要資源、巢狀綱要資源和外部綱要資源的對應有向圖表示。與之前一樣,在評估過程的每個步驟中,我們會將不屬於詞彙範圍的綱要和有向圖部分以灰色顯示。
評估過程從頂層綱要開始。此時的詞彙範圍是根綱要資源,而巢狀綱要資源和外部綱要資源都超出範圍。
接著,我們進入 anyOf
邏輯運算子的第一個分支,並追蹤 /anyOf/0/$ref
(以紅色標示)的參照到 /$defs/timestamp
。這個子綱要有自己的識別符,因此詞彙範圍會變成巢狀綱要資源,而根綱要資源和外部綱要資源都會超出範圍。
最後,我們回到根綱要資源,進入 anyOf
邏輯運算子的第二個分支,並追蹤 /anyOf/1/$ref
(以紅色標示)的遠端參照到 https://example.com/epoch
。這個外部綱要根據定義是一個獨立的綱要資源。因此,它會成為新的詞彙範圍。這次,根綱要資源及其巢狀綱要資源都超出範圍。
動態範圍
總而言之,綱要的詞彙範圍由其封閉的綱要資源組成。相比之下,綱要的動態範圍由目前已評估的綱要資源堆疊組成。回到我們將綱要比喻為圖形的說法,動態範圍對應於評估過程所訪問的節點的有序序列。
考慮以下範例序列。在左上方,一個名為 https://example.com/person
的根綱要資源宣告了兩個巢狀綱要資源:https://example.com/name
(位於 /properties/name
)和 https://example.com/age
(位於 /properties/age
)。在左下方,一個範例實例成功地針對該綱要進行驗證。請注意,該實例並未宣告 age
這個可選屬性。在右側,是有向圖,表示這些綱要資源之間的關係。與我們之前所做的類似,我們會將不屬於動態範圍的綱要和有向圖部分以灰色顯示。
評估過程從頂層綱要開始。此時的動態範圍是根綱要資源,而巢狀綱要資源都超出範圍。到目前為止,詞彙範圍和動態範圍對齊。
由於該實例定義了 name
屬性,因此我們進入 properties
運算子,進入位於 /properties/name
的子綱要。這個子綱要引入了一個新的綱要資源。因此,動態範圍現在由兩個部分組成,依序為根綱要資源和名為 https://example.com/name
的巢狀綱要資源。
與詞彙範圍相比,綱要的動態範圍並非總是可以靜態地確定,因為評估路徑通常取決於實例。例如,對於使用諸如 if
或 oneOf
之類的邏輯運算子關鍵字的綱要,範圍內的綱要資源有序序列可能會因實例的特性而異。
追蹤參照
到目前為止,我們已了解,對於詞彙範圍,追蹤參照包括放棄來源綱要的詞彙範圍,並進入目標綱要的詞彙範圍。相比之下,對於動態範圍,追蹤另一個綱要資源的參照涉及保留目前的動態範圍,並將目標綱要資源推入堆疊頂端。
在 Schema 資源內
與詞彙範圍一樣,如果參照指向同一個綱要資源中的子綱要,則動態範圍保持不變。換句話說,如果目標綱要資源與堆疊頂端的綱要資源相同,則動態範圍不會被修改。因此,在評估過程遇到對另一個綱要資源(無論是本機或遠端)的參照之前,*詞彙範圍和動態範圍會對齊*。
跨綱要資源
將簡單情況拋在一旁,讓我們考慮一個包含跨綱要資源的本機和遠端參照的範例。在左上方,一個範例實例和一個名為 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
的外部綱要資源,如左下方所示。在右側,是有向圖,表示這些綱要資源和動態範圍之間的關係。
與到目前為止的其他範例一樣,評估過程從頂層綱要開始。此時的動態範圍是根綱要資源,所有其他綱要資源都超出範圍。
由於該實例定義了 name
屬性,因此我們進入 properties
運算子,進入位於 /properties/name
的子綱要。這個子綱要引入了一個新的綱要資源。因此,動態範圍現在由 https://example.com
(根綱要資源)組成,後接 https://example.com/name
(位於 /properties/name
的巢狀綱要資源)。
https://example.com/name
綱要資源參照另一個巢狀綱要資源: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 Schema 至關重要。以下表格總結了最重要的重點:
比較點 | 詞彙範圍 | 動態範圍 |
---|---|---|
定義 | 由正在評估的綱要資源組成 | 由目前為止已評估的綱要資源堆疊組成 |
決定作用域 | 可以靜態決定,無需考慮實例 | 無法總是靜態決定。它可能會根據實例而變化 |
追蹤參考 | 包括放棄來源綱要的詞法作用域,並進入目標綱要的詞法作用域 | 包括將目標綱要資源推送到動態作用域堆疊的頂部 |
在未來的文章中,我們將建立在本文介紹的概念之上,以解開動態參考($dynamicRef
和 $dynamicAnchor
)的工作原理。
如果您喜歡這篇文章並想將您的 JSON Schema 技能應用於資料產業,請查看我的 O'Reilly 書籍:Unifying Business, Data, and Code: Designing Data Products using JSON Schema。您也可以在 LinkedIn 上與我聯繫。
圖片由 Christina Morillo 來自 Pexels。