使用 JSON Schema 建模繼承
我們最常被問到的問題可能是:「我如何在 JSON Schema 中建模繼承階層?」而我們最常見的答案是:「你不能。」
JSON Schema 本身並不是為此設計的。它是一個減法系統,更多的約束意味著更少的匹配,而資料建模往往是加法的,更多的定義意味著更多的匹配。這兩個系統本質上是不相容的。
然而,如果我們接受一些讓步,我們或許就能解決問題。
我們的模型
首先,我們將嘗試建模一些電腦週邊設備。在強型別語言中,我們可能會使用 Peripheral
基底類別來建模,該基底類別定義了所有週邊設備共有的若干屬性(通常還有函式)。然後,每個設備都將是這個基底類別的子類別。
就我們的目的而言,我們只會在基底類別上定義 name
屬性。也就是說,每個週邊設備都需要有一個名稱。
我將使用 TypeScript 作為程式碼範例,但這些概念也適用於其他語言。
1abstract class Peripheral {
2 name: string;
3 // ...
4}
現在我們可以透過繼承這個基底類別來定義其他週邊設備,例如 Mouse
和 Keyboard
。
1class Mouse extends Peripheral {
2 buttonCount: number;
3 wheelCount: number;
4 trackingType: "ball" | "optical";
5 // ...
6}
7
8class Keyboard extends Peripheral {
9 keyCount: number;
10 mediaButtons: boolean;
11 // ...
12}
這足以讓我們開始。
使用約束表示我們的模型
在 JSON Schema 中,理想情況下,我們希望為每個模型都有一個 schema。對於 peripheral,我們可能會嘗試類似這樣的方案
我正在對架構識別符使用 schema:
URI,因為這些架構在任何地方都無法存取。這是我們正在為即將推出的 JSON Schema 版本考慮的建議。如果您喜歡這種方法,請告訴我們。
但是 additionalProperties
關鍵字會造成問題。具體來說,「繼承」的架構(例如我們將為 Mouse
建構的架構)無法定義額外的屬性,而這正是它絕對需要做的。這根本行不通,解決方案很簡單,就是省略它。
但現在,_任何_ 具有 name
屬性的 JSON 物件都會被驗證為周邊設備。雖然不太正確,但我們可以接受。這給了我們第一個讓步。
模擬基底類別的 Schema 無法驗證實例是否代表該類別的衍生類別。
模擬衍生類別相當簡單:我們模擬衍生類別定義的內容,並將 $ref
加回基底 Schema。
{ "$schema": "https://json-schema.dev.org.tw/draft/2020-12/schema", "$id": "schema:keyboard", "$ref": "schema:peripheral", "properties": { "keys": { "type": "integer" }, "mediaButtons": { "type": "boolean" } }, "required": [ "keys", "mediaButtons" ], "unevaluatedProperties": false}
對於衍生的 Schema,我們可以使用 unevaluatedProperties
,因為這些沒有任何衍生自它們的 Schema。如果繼承層次結構較大,並且這些類別作為其他類別的基礎,我們就必須像對 schema:peripheral
所做的那樣,關閉 unevaluatedProperties
。只有在繼承樹的葉節點上才能檢查額外屬性。
此外,我們使用 unevaluatedProperties
而不是 additionalProperties
,因為我們需要它能夠「看到內部」的 $ref
,以識別出 name
已被評估為基底 Schema 的一部分。使用 additionalProperties
,name
將被拒絕。
這似乎很簡單,而我們只需要做出一個(相當容易的)讓步。
新增遞迴參考
如果我們的其中一個周邊設備本身可以連接其他周邊設備呢?例如,USB 集線器。
1class UsbHub extends Peripheral {
2 connectedDevices: Peripheral[];
3 // ...
4}
讓我們嘗試在 Schema 中對其進行建模
這可以運作,但請記住我們做出的第一個讓步嗎?這個 schema 允許任何具有字串 name
屬性的項目。 但這與 TypeScript 模型不一致。 TypeScript 模型表示 connectedDevices
只能容納衍生自 Peripheral
的類型。
雖然這對某些人來說可能足夠,但我認為它無法運作。 我想確保 connectedDevices
陣列中的項目僅為已知的週邊裝置類型。 為了做到這一點,我們需要另一個 schema。
僅支援已知的衍生
問題:我們需要一個 schema 來識別某些 JSON 代表我們已知的裝置類型之一。
解決方案:使用 oneOf
定義 schema,它會參考所有已知裝置類型 schema。
這個綱要相當基本。它只是說:「如果 JSON 符合這些裝置中的其中一個,那麼它就是已知的周邊裝置。」
我們現在可以在 schema:usbhub
中參考它。
現在可以正確驗證 USB 集線器及其連接的裝置。
問題在於,因為我無法動態地將項目新增到 oneOf
,所以我只能支援在開發時已知的裝置。在大多數情況下,這不是問題。然而,如果我計劃將其發佈在一個套件中供他人使用,它將無法支援他們建立的裝置。(我確實有一個解決方案,但它不是一個好的解決方案,所以我不會在這裡分享。)這給了我們第二個讓步
如果我們需要參考基底類別,我們只能支援事先已知的衍生類別。
一個意想不到的好處
要確定某些 JSON 是否為 Mouse
或 Keyboard
或 UsbHub
,可能會持有所有三個 schema,然後依序驗證每個 schema 以確定接收到哪一個。但是,我們對參照問題的解決方案實際上給了我們一個更好的選擇。
我們知道 schema:known-peripherals
可以驗證任何已知的周邊設備(因為我們設計它是為了做到這一點),但如果我們使用更詳細的輸出格式,它可以告訴我們我們得到的是哪種類型的周邊設備。
首先,我們透過檢視其子輸出節點中是否有 valid: true
來判斷哪個 oneOf
子模式通過驗證。我們知道它將會是一個 $ref
模式(因為它是一個僅包含 $ref
模式的 oneOf
),這表示該 $ref
模式的子輸出節點將代表周邊模式的輸出,其中包含周邊模式的 $id
URI。
因此,在單次驗證過程中,我們不僅可以得知它是否為任何一種受支援的周邊設備,而且我們可以辨別它是哪一種。一石二鳥。
那麼,JSON Schema 中是否可能實現繼承?
否。
以及,是的,如果我們可以接受以下事實:
模擬基底類別的 Schema 無法驗證實例是否代表該類別的衍生類別。
如果我們需要參考基底類別,我們只能支援事先已知的衍生類別。
我認為這些對於大多數人來說是可以接受的,但我也確信,有些人不可避免地會遇到這種方法行不通的情況。
這是目前我所見到建模繼承的最佳方法,而且我相當肯定,如果沒有一些新的功能,JSON Schema 無法 100% 正確地處理繼承。
如果您對於如何支援多型有其他想法,或者您認為多型被高估了,JSON Schema 不需要支援它,請加入我們在 IDL Vocabulary 儲存庫中的對話。
封面圖片由 Gerd Altmann 在 Pixabay 上提供