2022 年 8 月 31 日,星期三 ·6分鐘閱讀

使用動態參考來支援泛型型別

我們最常被問到的問題之一是如何在 JSON Schema 中表示強型別程式語言中的概念。類別階層、多型、泛型等。這些概念定義了強型別語言,並影響我們的資料模型。

這篇文章的主題可以應用於任何具有已定義資料模型並支援類似泛型型別的程式設計範例。這很可能是關於程式語言中的資料建模和 JSON Schema 之間關係的一系列不相關文章中的第一篇。

今天,我想討論泛型或模板或其他您可能聽過的標籤的概念。首先,我們先來介紹一下我所謂的「泛型型別」。這不是要上課教導它們;我只是想確保我們都在同一個起跑點上。

泛型型別

我所謂的「泛型型別」是指許多程式語言中存在的一種功能,它可以建立一種型別,該型別需要一個或多個輔助型別的額外資訊才能完成。在物件導向程式設計中,泛型可以應用於服務以及資料模型,但由於我們使用 JSON Schema 來描述資料模型,因此我們可以相當肯定,我們關心的此用例中的泛型型別是包裝器和容器。

在 .Net (C#) 中,這些以型別名稱結尾的角括號表示,例如 List<T>Dictionary<TKey, TValue>,其中 TTKeyTValue 代表輔助型別。(這些範例都是容器型別,但您也可以對信封型別執行此操作,例如 Cloud Events (例如 CloudEvent<T>)。)

在 C++ 中,這些型別稱為「模板」,並以關鍵字 template 表示,後面接著輔助型別資訊,也以角括號括起來

1template <class T>
2class List { ... }
3
4template <class TKey, class TValue>
5class Dictionary { ... }

Typescript 也有這個概念,並且大多遵循 C# 語法。

這些型別接著透過定義所需的輔助型別來完成。這通常是透過將型別預留位置 (例如 T) 替換為輔助型別來完成,因此 C# 範例為 List<T>Dictionary<string, int>。其中一個有趣的結果是,泛型型別本身無法被實例化:它需要輔助型別,以便編譯器或腳本引擎 (或任何執行程式碼的引擎) 可以了解關於型別的各種資訊,例如記憶體佔用空間。

那麼,問題是,如何在 JSON Schema 中表示部分定義的型別。

使用 $dynamicRef$dynamicAnchor 來表示泛型

動態關鍵字,統稱為 $dynamic*,啟用一般在評估時間之前無法解析的參考,這與 $ref 不同,後者可以使用 schema 靜態解析。通常,當 schema 也定義條件式 (if/then/else) 時,這是最明顯的,條件式會根據正在評估的 JSON 實例變更解析。

但是,為了支援泛型,我們想要以稍微不同的方式使用這種動態行為。我們將使用的策略是分為兩個步驟的程序。

  1. 對於泛型型別本身,我們將撰寫一個 schema,其中的參考一開始會解析為一個總是會驗證失敗的子 schema。
  2. 對於每個衍生,我們會撰寫一個子 schema,其
    • 參考 #1 中的泛型型別 schema
    • 為描述輔助型別的相同參考定義一個新的子 schema

為了實際了解,我們來為上述的 List<T> 撰寫 schema。然後,我們將再撰寫兩個 schema,利用它來協助我們定義 List<string>List<int>

泛型 Schema:List<T>

我們從一個簡單的事物清單開始。

schema
{ "$schema": "https://json-schema.dev.org.tw/draft/2020-12/schema", "$id": "https://json-schema.example/list-of-t", "type": "array"}

現在我們定義項目。這裡 $dynamic* 會為我們做一些工作。

schema
{ "$schema": "https://json-schema.dev.org.tw/draft/2020-12/schema", "$id": "https://json-schema.blog/list-of-t", "$defs": { "content": { "$dynamicAnchor": "T", "not": true } }, "type": "array", "items": { "$dynamicRef": "#T" }}

注意 我在這裡使用 T 是為了與 C# 的 List<T> 匹配,以便更好地說明正在發生的事情。你可以隨意命名它。

如果我們只針對這個 schema 驗證一個實例,"$dynamicRef": "#T" 會解析為在 /$defs/content 中包含 "$dynamicAnchor": "T" 的子 schema。在這種情況下,$dynamicRef$dynamicAnchor 的作用就像 $ref$anchor 一樣。

"not": true 表示所有實例都將無法通過驗證。通常,為了確保所有實例都無法通過驗證,我們會使用 false schema,但在這種情況下,我們需要包含一個動態錨點,因此簡單的 false 無法工作。我認為 "not": true 可能是最簡潔的替代方案,但如果你覺得 "allOf": [ false ] 更合理,你也可以使用它。

注意 空陣列仍然會通過此 schema 的驗證,但任何有項目的陣列都會失敗。

你也可以使用多個動態錨點來支援像 Dictionary<TKey, TValue> 這樣需要多個二級類型的類型。

schema
{ "$schema": "https://json-schema.dev.org.tw/draft/2020-12/schema", "$id": "https://json-schema.blog/list-of-t", "$defs": { "key": { "$dynamicAnchor": "TKey", "not": true }, "value": { "$dynamicAnchor": "TValue", "not": true } }, "type": "array", "items": { "type": "object", "properties": { "key": { "$dynamicRef": "#TKey" }, "value": { "$dynamicRef": "#TValue" } } }}

這就是泛型類型的全部內容。當我們定義內容時,就會產生神奇的效果。

定義內容

如前所述,我們需要一個引用 list-of-t 的 schema,並且還需要提供 T 的定義。讓我們為 List<string> 寫一個。

schema
{ "$schema": "https://json-schema.dev.org.tw/draft/2020-12/schema", "$id": "https://json-schema.blog/list-of-string", "$defs": { "string-items": { "$dynamicAnchor": "T", "type": "string" } }, "$ref": "list-of-t"}

這是運作方式

  1. 當開始評估時,它會建立一個「動態範圍」,從根綱要(list-of-string)開始,並在整個評估過程中保持不變。
  2. 這個根綱要定義了一個 "$dynamicAnchor": "T"
  3. 然後,評估會透過 $ref 參照到通用綱要 list-of-t。這是一個新的語法範圍,但動態範圍保持不變。
  4. 這個通用綱要也宣告了 "$dynamicAnchor": "T",但這個動態錨點已經定義過了,因此新的宣告會被忽略。
  5. 當評估遇到 "$dynamicRef": "#T" 時,它會使用動態範圍開頭的第一個。

如果我們想要 int 項目而不是 string,我們只需要建立一個新的綱要,其中帶有 $dynamicAnchor 的子綱要定義一個整數即可。

schema
{ "$schema": "https://json-schema.dev.org.tw/draft/2020-12/schema", "$id": "https://json-schema.blog/list-of-int", "$defs": { "int-items": { "$dynamicAnchor": "T", "type": "integer" } }, "$ref": "list-of-t"}

結論

透過使用 $dynamicRef$dynamicAnchor,您不需要為結構相同但內容類型不同的類別編寫完整的 schema。相反地,您可以為結構編寫部分、可重複使用的 schema,這會使完整定義的 schema 顯著縮小並更容易維護。

旁註 在 Draft 2020-12 中,為了使 $dynamicRef 能夠運作,需要在通用 schema 中包含 $dynamicAnchor。在未來版本中,此要求將會移除,因為它並非嚴格必要:任何解析嘗試都只會失敗。(此要求是從其 Draft 2019-09 前身 $recursive* 遺留下來的。)然而,對於建立泛型類型的特定應用,我仍然會保留它,因為它可以類比為無法實例化泛型類型,例如 List<T>。最終結果是相同的(驗證失敗),但我認為加入它能更明確地描述意圖。

封面照片由 Nick FewingsUnsplash 上拍攝,並由我稍作編輯。 😁