2022 年 3 月 21 日星期一 ·16分鐘閱讀

一切都從適用性開始 - JSON Schema 基礎知識第 1 部分

「驗證從將根模式應用於完整的實例文件開始。適用器關鍵字將子模式應用於實例位置。」- 借鑑自 JSON Schema in 5 minutes

JSON Schema 的主要用例是驗證。因此,必須準確理解驗證過程如何發生。讓我們花一點時間來正確理解適用性的基本 JSON Schema 概念。

適用器關鍵字

JSON Schema 由許多關鍵字組成。這些關鍵字可以分為幾種類別,其中一類是「適用器」。在物理意義上,「適用器」是用於將一種物質引入另一種物質的東西。例如,可以使用布將拋光劑引入漂亮的木桌。布是適用器。拋光劑透過布應用於桌子。

JSON Schema 中的適用器關鍵字類似於布,但它們是將模式應用於實例資料中的位置(或僅是「實例位置」)。

從所有內容開始

JSON Schema 的驗證過程從將整個 JSON Schema 應用於整個實例開始。此應用(模式到實例)的結果應產生布林判斷,即驗證結果。

JSON Schema 可以是布林值或物件。在上面提到的介紹性文章中,我們注意到布林模式 truefalse 如何產生相同的判斷結果(分別為 true 和 false),而與實例資料無關。我們還注意到等效的物件模式分別是 { }{ "not": { } }。(not 關鍵字反轉判斷結果。)

詞彙檢查

「判斷」是對事實的陳述。這在電腦運算中用於參考測試結果。該測試可能稱為「X 為 1」。如果測試通過,則判斷為 true!

當我們在應用方面談論整個模式時,我們通常將其稱為「根模式」。這是因為應用於特定實例位置的其他模式不同,我們稱之為「子模式」。區分根模式和子模式可以讓我們清楚地溝通我們談論的是哪個 JSON Schema,以及何時使用該模式作為驗證過程的一部分。

以下範例假設使用 JSON Schema 2020-12。如果有一些您應該了解的 JSON Schema 先前版本(或草案)的事項,將會突出顯示。

子模式應用 - 驗證物件和陣列

如果您的 JSON 實例是物件或陣列,您可能想要驗證物件的值或陣列中的項目。在本簡介中,您將使用 propertiesitems 關鍵字和子模式。

驗證物件

讓我們來看一個例子。這是我們的實例資料。

資料
{ "id": 1234, "name": "Bob", "email": "[email protected]", "isEmailConfirmed": true}

為了建立模式的基本結構,我們複製結構並將其放置在 properties 關鍵字下,將值變更為空物件,然後定義類型。

資料
{ "properties": { "id": { "type": "number" }, "name": { "type": "string" }, "email": { "type": "string" }, "isEmailConfirmed": { "type": "boolean" } }}

資料
{ "id": 1234, "name": "Bob", "email": "[email protected]", "isEmailConfirmed": "true"}// isEmailConfirmed 應該是一個布林值 (Boolean),而不是字串。// 將導致驗證錯誤。

資料
{ "properties": { "id": { "type": "number" }, "name": { "type": "string" }, "email": { "type": "string" }, "isEmailConfirmed": { "type": "boolean" } }, "required": [ "id", "name", "email" ]}

現在我們可以確信,如果缺少必要的欄位,驗證將會失敗。但是如果有人在可選欄位中犯了錯誤怎麼辦?

資料
{ "id": 1234, "name": "Bob", "email": "[email protected]", "isEmaleConfirmed": "true"}// 鍵 "isEmaleConfirmed" 的拼寫錯誤。// 因為適用性而驗證通過。

我們的欄位 isEmailConfirmed 具有字串值,而不是布林值,但驗證仍然通過。如果你仔細看,你會發現鍵的拼寫錯誤為 "isEmaleConfirmed"。誰知道為什麼,但我們就遇到了這種情況。

幸運的是,使用我們的 Schema 來處理這個問題很簡單。additionalProperties 關鍵字可以讓您防止在物件中使用超出 properties 中定義的屬性(或鍵)。

資料
{ "properties": { "id": { "type": "number" }, "name": { "type": "string" }, "email": { "type": "string" }, "isEmailConfirmed": { "type": "boolean" } }, "required": [ "id", "name", "email" ], "additionalProperties": false}

additionalProperties 的值不只是布林值,而是一個 Schema。這個子 Schema 值會套用到實例物件中所有未在我們的範例的 properties 物件中定義的值。您可以使用 additionalProperties 來允許額外的屬性,但將它們的值限制為字串。

這裡做了一些簡化,以幫助我們理解我們想要學習的概念。如果您想更深入地了解,請查看我們關於 additionalProperties 的學習資源。

最後,如果我們預期得到一個物件,但卻得到一個陣列或其他非物件類型,那該怎麼辦?

資料
[ { "id": 1234, "name": "Bob", "email": "[email protected]", "isEmaleConfirmed": "true" }]// 陣列不是物件...

你可能會覺得通過驗證很令人驚訝!但為什麼呢!?

到目前為止我們探討的三個關鍵字,propertiesrequiredadditionalProperties,只定義了對物件的約束,並且在遇到其他類型時會被忽略。如果我們想確保類型如我們預期的那樣(一個物件),我們也需要指定這個約束!

資料
{ "type": ["object"], "properties": { "id": { "type": "number" }, "name": { "type": "string" }, "email": { "type": "string" }, "isEmailConfirmed": { "type": "boolean" } }, "required": [ "id", "name", "email" ], "additionalProperties": false}

總而言之,為了達到最健全的驗證,我們必須表達所有我們需要的約束。 假設 properties 關鍵字僅在鍵匹配時才應用其 Schema 值,且僅在目前實例位置是物件時才應用,我們需要確保其他約束到位,以捕捉其他可能的情況。

請注意,type 接受一個類型陣列。 您的實例可能允許是物件或陣列,並且可以在同一個 Schema 物件中定義兩者的約束。

驗證陣列

在這份介紹中,我們將只涵蓋 JSON Schema 2020-12 的運作方式。如果您使用的是較舊的版本,包括「draft-7」或更早的版本,您可能會從深入研究陣列驗證的學習資源中獲益。

讓我們回到之前的範例資料,其中我們得到的是一個陣列而不是一個物件。假設現在我們的資料只允許是一個陣列。

為了驗證陣列中的每個項目,我們需要使用 items 關鍵字。items 關鍵字的值是一個 Schema。此 Schema 會應用於陣列中的所有項目。

資料
{ "items": { "type": ["object"], "properties": { "id": { "type": "number" }, "name": { "type": "string" }, "email": { "type": "string" }, "isEmailConfirmed": { "type": "boolean" } }, "required": [ "id", "name", "email" ], "additionalProperties": false }}

如同 properties 的適用規則,items 的值 Schema 僅在驗證的實例位置為陣列時才適用。如果我們想要確保它是一個陣列,我們需要透過在我們的 Schema 中新增 "type": ["array"] 來指定約束。

還有其他適用於陣列的關鍵字,但如果我繼續詳細解釋所有這些關鍵字,這篇文章可能會變成一本參考書!繼續往下看...

應用但修改 - 具有子模式的布林邏輯

JSON Schema 應用程式關鍵字不僅可以應用子模式並採用產生的布林斷言。應用程式關鍵字可以有條件地應用子模式,並使用布林邏輯組合或修改任何產生的斷言。

讓我們來看看最基本的應用程式關鍵字:allOfanyOfoneOf

這些關鍵字都將一個模式陣列作為其值。陣列中的所有模式都會應用於實例。

我們將依序探討每個關鍵字,並了解它們之間的差異。

allOf 陣列應用每個模式項目後,驗證(斷言)結果會以邏輯 AND 組合。如關鍵字所示,陣列中的所有模式都必須產生 true 的斷言。如果任何一個模式斷言 false(驗證失敗),則 allOf 關鍵字也會斷言 false。

這聽起來很簡單,但讓我們看一些範例。

資料
{ "allOf": [ true, true, true]}
資料
{ "allOf": [ true, false, true]}

請記住: 布林值是一個有效的模式,無論實例資料為何,它總是會產生其值的斷言結果。

我們的第一個 "allOf" 範例顯示,陣列中有三個子模式,它們都是 true。結果會使用布林邏輯 AND 運算子組合。來自 allOf 關鍵字的最終斷言是 true

我們的第二個 "allOf" 範例顯示,陣列中的第二個項目是 false 布林模式。來自 allOf 關鍵字的最終斷言是 false

此範例中的 truefalse 布林模式可以是任何通過或未通過驗證的子模式。使用布林模式可讓我們更容易地示範這些應用程式關鍵字的布林邏輯使用方式。

讓我們再次使用這兩個範例,但這次使用 anyOf 而不是 allOf

資料
{ "anyOf": [ true, true, true]}
資料
{ "anyOf": [ true, false, true]}

每個模式的斷言結果會使用布林邏輯 OR 運算子組合。如果任何一個最終斷言為 trueanyOf 就會傳回 true 的斷言。如果所有最終斷言都為 falseanyOf 就會傳回 false 的斷言。

無論您是否覺得這很直觀,我們都來看一個真值表,了解這兩個關鍵字的行為方式。這會變得有點像數學,但不多,我保證!(這可能看起來有點過頭或過度深入,但這是基本知識。請繼續閱讀。)

「allOf」的真值表
「anyOf」的真值表

真值表有時有助於理解布林邏輯,例如查看等效性,例如 !(A AND B)!A OR !B 相同。

上面的兩個真值表代表 allOfanyOf 關鍵字的布林邏輯。A、B 和 C 代表我們先前範例中的三個子模式,以及它們斷言結果的所有可能組合。T 和 F 代表 truefalse 的斷言。

(請記住,這些值是子模式,但我們使用布林模式來使斷言結果顯而易見)。

山形符號是數學中的符號,其中向上山形符號代表「AND」,向下山形符號代表「OR」。最右邊的欄代表根據標題中的布林邏輯所產生的整體斷言結果。

我們可以直觀地看到這兩個關鍵字如何組合其子模式的布林斷言結果。

allOf - 如果「所有」斷言為 true,則組合的斷言為 true,否則為 false

anyOf - 如果「任何」斷言為 true,則組合的斷言為 true,否則為 false

但是 oneOf 呢?該關鍵字使用的布林邏輯是互斥 OR...某種程度上。簡稱「XOR」常用於電子產品,但不能準確翻譯為「只能有一個為 true」,這才是 JSON Schema 中 oneOf 的意圖。

這是兩個輸入的真值表(如果 oneOf 的陣列值只包含兩個子模式值)。

XOR 的真值表

看起來沒錯吧?但是,如果我們加入另一個「輸入」,使其成為奇數呢?

三個輸入的 XOR 真值表

看起來大致正確,但請注意,如果所有斷言都為 true,則最終斷言也為「true」!這不是我們想要的,但這是數學上正確的結果。因此,我們必須擴展邏輯定義以包含「... AND NOT(A && B && C)」。我們最終的真值表如下所示。

「oneOf」的真值表 - (a xor b xor c) & ! (a && b && c)

好多了!但您為什麼應該關心這個?

嗯,現在我們有了理解一個相當常見問題的方法,以及上面所有新的(或經過修訂的)知識來解決它。

整合所有內容 - 避免 oneOf 的陷阱

讓我們回到我們的人員資料陣列,修改它,並假設它代表一個老師和學生的陣列。

資料
[ { "name": "Bob", "email": "[email protected]", "isStudent": true, "year": 1 }, { "name": "Alice", "email": "[email protected]", "isTeacher": true, "class": "CS101" }]

首先,讓我們做跟建立第一個綱要時一樣的事情。複製實例,並將其巢狀放置於 properties 之下。我們也需要將這些物件綱要巢狀放置於 oneOf 之下,就像我們看到 allof 的使用方式一樣。然後將所有這些巢狀放置於 items 之下,以將綱要應用於陣列中的每個項目... 好的,讓我們直接看看...

資料
{ "items": { "oneOf":[ { "properties": { "name": { "type": "string" }, "email": { "type": "string" }, "isStudent": { "type": "boolean" }, "year": { "type": "number" } } }, { "properties": { "name": { "type": "string" }, "email": { "type": "string" }, "isTeacher": { "type": "boolean" }, "class": { "type": "number" } } } ] }}

現在,讓我們看看當我們嘗試使用新的綱要驗證我們的實例時會發生什麼...

1should match exactly one schema in oneOf.
2oneOf at "#/items/oneOf"
3Instance location: "/0"

哎呀!那不是我們想要的!

但為什麼它沒有作用?為什麼實例沒有通過驗證?

我們知道什麼?

驗證器正在「快速失敗」。這表示它會在第一個錯誤後停止。

正在評估的實例位置是陣列中的第一個項目。

錯誤告訴我們,陣列中的第一個項目與我們 oneOf 中找到的子綱要完全不匹配。這表示它對兩者都成功驗證。

我們實例陣列中的第一個項目被識別為學生,因此應該只通過 oneOf 中的第一個子綱要。那麼為什麼在應用第二個子綱要時它是有效的?

讓我們回顧一下。properties 關鍵字會根據實例物件中匹配的鍵來應用其綱要(即值)。我們先前探討的含義是,僅在 properties 物件中擁有一個鍵並不表示該鍵在實例中是必需的。

當你將 oneOf 中的第二個子綱要應用於實例時,沒有任何限制會導致它無法通過驗證,因此它通過了驗證。如果所有子綱要都認為實例位置有效,則 oneOf 無法通過驗證,因為它不是「一個且僅一個」,就像「真正的互斥或」一樣。

現在換你試試

我們可以像之前一樣使用相同的方法,以確保我們的子綱要有足夠的限制。試試看,看看你是否能讓驗證如預期般運作。

此連結已預先載入你的起始綱要和實例。如果你遇到任何困難,請透過 SlackTwitter 告訴我。

總結

綱要幾乎總是會有一些子綱要。

識別子綱要在哪裡以及如何將它們應用於不同的實例位置,可以讓你評估和解析有問題的綱要。

你可以將幾乎任何子綱要本身作為一個綱要,並測試驗證過程。(當子綱要有參考時,這可能並非總是可行。)

應用程式關鍵字不僅可以傳達來自子綱要的斷言結果,還可以結合並以不同的方式修改它們,通常使用布林邏輯,以提供自己的斷言。

我真的很享受能夠與你分享我們基礎系列的第一篇文章,並且我希望你覺得它有足夠的價值,讓你回來閱讀本系列的下一篇文章。

你可以在 JSON Schema Fundamentals 儲存庫中找到所有的實例和綱要範例。

歡迎所有回饋。如果你有任何問題或意見,可以在 JSON Schema Slack 上找到我,或者在 Twitter 上與我聯繫 @relequestual

有用的連結和進一步閱讀

由 Heidi Fin 拍攝,來自 Unsplash