2023 年 11 月 8 日 星期三 ·7分鐘閱讀

解讀 JSON Schema 輸出

過去幾年,我收到了許多關於 JSON Schema 輸出的問題(和聲稱的錯誤),也進行了不少討論,其中最常見的問題是:「為什麼我的通過驗證卻包含錯誤?」

讓我們深入探討。

上次我們談論輸出時,是為了宣布從 2019-09/2020-12 版本開始的變更。我將使用新的格式,因為它更容易閱讀且更簡潔。

沒問題

在我們深入探討輸出可能令人困惑的地方之前,我們先回顧一下順利的情況,也就是以下兩種情況:

  • 所有子節點都有效,因此整體驗證有效,或者
  • 一個或多個子節點無效,因此整體驗證無效。

這些情況非常容易理解,因此是一個很好的起點。

schema
{ "$schema": "https://json-schema.dev.org.tw/draft/2020-12/schema", "$id": "https://json-schema.dev.org.tw/blog/interpreting-output/example1", "type": "object", "properties": { "foo": { "type": "boolean" }, "bar": { "type": "integer" } }, "required": [ "foo" ]}

這是一個相當基本的綱要,這是一個通過的實例

資料
{ "foo": true, "bar": 1 }

與輸出

資料
{ "valid": true, "evaluationPath": "", "schemaLocation": "https://json-schema.dev.org.tw/blog/interpreting-output/example1#", "instanceLocation": "", "annotations": { "properties": [ "foo", "bar" ] }, "details": [ { "valid": true, "evaluationPath": "/properties/foo", "schemaLocation": "https://json-schema.dev.org.tw/blog/interpreting-output/example1#/properties/foo", "instanceLocation": "/foo" }, { "valid": true, "evaluationPath": "/properties/bar", "schemaLocation": "https://json-schema.dev.org.tw/blog/interpreting-output/example1#/properties/bar", "instanceLocation": "/bar" } ]}

/details 中的所有子綱要輸出節點都是有效的,而且根也是有效的,大家都開心。

同樣地,這是一個失敗的實例 (因為 bar 是一個字串)

資料
{ "foo": true, "bar": "value" }

與輸出

資料
{ "valid": false, "evaluationPath": "", "schemaLocation": "https://json-schema.dev.org.tw/blog/interpreting-output/example1#", "instanceLocation": "", "details": [ { "valid": true, "evaluationPath": "/properties/foo", "schemaLocation": "https://json-schema.dev.org.tw/blog/interpreting-output/example1#/properties/foo", "instanceLocation": "/foo" }, { "valid": false, "evaluationPath": "/properties/bar", "schemaLocation": "https://json-schema.dev.org.tw/blog/interpreting-output/example1#/properties/bar", "instanceLocation": "/bar", "errors": { "type": "Value is \"string\" but should be \"integer\"" } } ]}

/details/1 的子模式輸出無效,且根也無效。雖然我們可能因為它失敗而感到有些不快,但至少我們理解了原因。

那麼情況總是這樣嗎?一個通過驗證的子模式會存在失敗的子模式嗎?絕對會!

更複雜的情況

我們可以有無數種方式創建一個模式和一個實例,使其在產生失敗節點的同時仍然通過驗證。幾乎所有這些方式都與呈現多個選項的關鍵字(anyOfoneOf)或條件式(ifthenelse)有關。這些情況,特別是,具有 *設計成* 會失敗,但仍產生成功驗證結果的子模式。

在這篇文章中,我將重點介紹以下條件式模式,但相同的概念適用於包含「多個選項」關鍵字的模式。

schema
{ "$schema": "https://json-schema.dev.org.tw/draft/2020-12/schema", "$id": "https://json-schema.dev.org.tw/blog/interpreting-output/example2", "type": "object", "properties": { "foo": { "type": "boolean" } }, "required": ["foo"], "if": { "properties": { "foo": { "const": true } } }, "then": { "required": ["bar"] }, "else": { "required": ["baz"] }}

這個模式表示,如果 foo 為 true,我們還需要一個 bar 屬性,否則我們需要一個 baz 屬性。因此,以下兩種情況都是有效的:

資料
{ "foo": true, "bar": 1 }
資料
{ "foo": false, "baz": 1 }

當我們查看第一個實例的驗證輸出時,我們會得到類似於上一節中「快樂路徑」的輸出:所有輸出節點都具有 valid: true,一切都說得通。

但是,查看第二個實例的驗證輸出(如下),我們注意到 /if 子模式的輸出節點具有 valid: false。但整體驗證通過了。

資料
{ "valid": true, "evaluationPath": "", "schemaLocation": "https://json-schema.dev.org.tw/blog/interpreting-output/example2#", "instanceLocation": "", "annotations": { "properties": [ "foo" ] }, "details": [ { "valid": true, "evaluationPath": "/properties/foo", "schemaLocation": "https://json-schema.dev.org.tw/blog/interpreting-output/example2#/properties/foo", "instanceLocation": "/foo" }, { "valid": false, "evaluationPath": "/if", "schemaLocation": "https://json-schema.dev.org.tw/blog/interpreting-output/example2#/if", "instanceLocation": "", "details": [ { "valid": false, "evaluationPath": "/if/properties/foo", "schemaLocation": "https://json-schema.dev.org.tw/blog/interpreting-output/example2#/if/properties/foo", "instanceLocation": "/foo", "errors": { "const": "Expected \"true\"" } } ] }, { "valid": true, "evaluationPath": "/else", "schemaLocation": "https://json-schema.dev.org.tw/blog/interpreting-output/example2#/else", "instanceLocation": "" } ]}

這怎麼可能呢?

輸出包含原因

通常,比實例通過驗證的簡單結果更重要的是 *它為什麼* 通過驗證,尤其是在結果不是預期的情況下。為了支持這一點,有必要包括所有相關的輸出節點。

如果我們從結果中排除失敗的輸出節點,

資料
{ "valid": true, "evaluationPath": "", "schemaLocation": "https://json-schema.dev.org.tw/blog/interpreting-output/example2#", "instanceLocation": "", "annotations": { "properties": [ "foo" ] }, "details": [ { "valid": true, "evaluationPath": "/properties/foo", "schemaLocation": "https://json-schema.dev.org.tw/blog/interpreting-output/example2#/properties/foo", "instanceLocation": "/foo" }, { "valid": true, "evaluationPath": "/else", "schemaLocation": "https://json-schema.dev.org.tw/blog/interpreting-output/example2#/else", "instanceLocation": "" } ]}

我們會看到評估了 /else 子模式,從中我們可以推斷出 /if 子模式 *一定* 失敗了。但是,我們沒有關於 *為什麼* 失敗的資訊,因為該子模式的輸出被省略了。但是,回頭看看完整的輸出,很明顯 /if 子模式失敗的原因是它預期 foo 為 true。

因此,輸出必須保留所有已評估子模式的節點。

同樣重要的是要注意,規範指出,if 關鍵字不會直接影響整體驗證結果。

關於格式的注意事項

在結束之前,還有一個方面對於閱讀輸出可能很重要:格式。以上所有範例都使用 *階層式* 格式(以前稱為 *Verbose*)。但是,根據您的需求和偏好,您可能想要使用 *清單* 格式(以前稱為 *Basic*)。

這是 *清單* 格式中簡單模式的輸出:

資料
{ "valid": false, "details": [ { "valid": false, "evaluationPath": "", "schemaLocation": "https://json-schema.dev.org.tw/blog/interpreting-output/example1#", "instanceLocation": "" }, { "valid": true, "evaluationPath": "/properties/foo", "schemaLocation": "https://json-schema.dev.org.tw/blog/interpreting-output/example1#/properties/foo", "instanceLocation": "/foo" }, { "valid": false, "evaluationPath": "/properties/bar", "schemaLocation": "https://json-schema.dev.org.tw/blog/interpreting-output/example1#/properties/bar", "instanceLocation": "/bar", "errors": { "type": "Value is \"string\" but should be \"integer\"" } } ]}

這很容易閱讀和處理,因為所有輸出節點都在單一層級上。要查找錯誤,您只需要在 /details 中掃描任何包含錯誤的節點。

這是 *清單* 格式中條件式模式的輸出:

資料
{ "valid": true, "details": [ { "valid": true, "evaluationPath": "", "schemaLocation": "https://json-schema.dev.org.tw/blog/interpreting-output/example2#", "instanceLocation": "", "annotations": { "properties": [ "foo" ] } }, { "valid": true, "evaluationPath": "/properties/foo", "schemaLocation": "https://json-schema.dev.org.tw/blog/interpreting-output/example2#/properties/foo", "instanceLocation": "/foo" }, { "valid": false, "evaluationPath": "/if", "schemaLocation": "https://json-schema.dev.org.tw/blog/interpreting-output/example2#/if", "instanceLocation": "" }, { "valid": true, "evaluationPath": "/else", "schemaLocation": "https://json-schema.dev.org.tw/blog/interpreting-output/example2#/else", "instanceLocation": "" }, { "valid": false, "evaluationPath": "/if/properties/foo", "schemaLocation": "https://json-schema.dev.org.tw/blog/interpreting-output/example2#/if/properties/foo", "instanceLocation": "/foo", "errors": { "const": "Expected \"true\"" } } ]}

在這裡,很明顯我們不能只掃描錯誤,因為我們必須考慮這些錯誤的來源。最後一個輸出節點中的錯誤僅與 /if 子模式相關,如前所述,這不會影響驗證結果。

總結

JSON Schema 輸出提供您所有需要的資訊,以便了解驗證結果以及評估器如何得出該結果。然而,了解如何閱讀輸出需要理解為什麼所有這些部分都存在。

如果您有任何問題,請隨時在我們的 Slack 工作區(頁腳中有連結)提問,或開啟討論

所有輸出都是使用我的線上評估器 https://json-everything.net/json-schema 產生的。

封面圖片由 Tim GouwUnsplash 上提供