在过去几年里,我收到了很多关于 JSON Schema 输出的疑问(以及声称的 bug),并进行了许多讨论,其中最常见的问题是,“为什么我通过验证的实例会包含错误?”
让我们深入研究一下。
上次我们 谈论输出 时,是为了宣布 2019-09/2020-12 版本的更改。我将使用新的格式,因为它更易于阅读,也更紧凑。
没问题
在深入研究输出可能造成困惑的地方之前,让我们回顾一下最常见的场景,即:
- 所有子节点均有效,因此整体验证有效,或者
- 一个或多个子节点无效,因此整体验证无效。
这些情况很容易理解,因此可以作为良好的起点。
schema
{ "$schema": "https://json-schema.fullstack.org.cn/draft/2020-12/schema", "$id": "https://json-schema.fullstack.org.cn/blog/interpreting-output/example1", "type": "object", "properties": { "foo": { "type": "boolean" }, "bar": { "type": "integer" } }, "required": [ "foo" ]}
这是一个非常基本的 schema,以下是一个通过验证的实例:
{ "foo": true, "bar": 1 }
输出为
{ "valid": true, "evaluationPath": "", "schemaLocation": "https://json-schema.fullstack.org.cn/blog/interpreting-output/example1#", "instanceLocation": "", "annotations": { "properties": [ "foo", "bar" ] }, "details": [ { "valid": true, "evaluationPath": "/properties/foo", "schemaLocation": "https://json-schema.fullstack.org.cn/blog/interpreting-output/example1#/properties/foo", "instanceLocation": "/foo" }, { "valid": true, "evaluationPath": "/properties/bar", "schemaLocation": "https://json-schema.fullstack.org.cn/blog/interpreting-output/example1#/properties/bar", "instanceLocation": "/bar" } ]}
在 /details
中的所有子模式输出节点都是有效的,根节点也是有效的,大家都满意。
类似地,这是一个失败的实例(因为 bar
是一个字符串)
{ "foo": true, "bar": "value" }
输出为
{ "valid": false, "evaluationPath": "", "schemaLocation": "https://json-schema.fullstack.org.cn/blog/interpreting-output/example1#", "instanceLocation": "", "details": [ { "valid": true, "evaluationPath": "/properties/foo", "schemaLocation": "https://json-schema.fullstack.org.cn/blog/interpreting-output/example1#/properties/foo", "instanceLocation": "/foo" }, { "valid": false, "evaluationPath": "/properties/bar", "schemaLocation": "https://json-schema.fullstack.org.cn/blog/interpreting-output/example1#/properties/bar", "instanceLocation": "/bar", "errors": { "type": "Value is \"string\" but should be \"integer\"" } } ]}
在 /details/1
的子模式输出无效,根节点也无效,虽然我们可能不太高兴因为它失败了,但至少我们知道为什么。
那么情况总是这样吗?通过验证的子模式可能会有失败的子模式吗?绝对有可能!
更多复杂性
我们可以创建无限种模式和实例,这些模式和实例可以同时通过验证,并输出失败的节点。几乎所有这些都与提供多种选择的关键字有关(anyOf
或 oneOf
)或条件语句(if
、then
和 else
)。这些情况,特别是,具有旨在失败但仍然产生成功验证结果的子模式。
对于这篇文章,我将重点关注以下条件模式,但相同的想法也适用于包含“多个选项”关键字的模式。
schema
{ "$schema": "https://json-schema.fullstack.org.cn/draft/2020-12/schema", "$id": "https://json-schema.fullstack.org.cn/blog/interpreting-output/example2", "type": "object", "properties": { "foo": { "type": "boolean" } }, "required": ["foo"], "if": { "properties": { "foo": { "const": true } } }, "then": { "required": ["bar"] }, "else": { "required": ["baz"] }}
此模式表示如果 foo
为真,我们还需要一个 bar
属性,否则我们需要一个 baz
属性。因此,以下两个都是有效的
{ "foo": true, "bar": 1 }
{ "foo": false, "baz": 1 }
当我们查看第一个实例的验证输出时,我们得到的输出类似于上一节中的快乐路径:所有输出节点都具有 valid: true
,一切都有意义。
但是,查看第二个实例的验证输出(如下),我们注意到 /if
子模式的输出节点具有 valid: false
。但是,总体的验证通过了。
{ "valid": true, "evaluationPath": "", "schemaLocation": "https://json-schema.fullstack.org.cn/blog/interpreting-output/example2#", "instanceLocation": "", "annotations": { "properties": [ "foo" ] }, "details": [ { "valid": true, "evaluationPath": "/properties/foo", "schemaLocation": "https://json-schema.fullstack.org.cn/blog/interpreting-output/example2#/properties/foo", "instanceLocation": "/foo" }, { "valid": false, "evaluationPath": "/if", "schemaLocation": "https://json-schema.fullstack.org.cn/blog/interpreting-output/example2#/if", "instanceLocation": "", "details": [ { "valid": false, "evaluationPath": "/if/properties/foo", "schemaLocation": "https://json-schema.fullstack.org.cn/blog/interpreting-output/example2#/if/properties/foo", "instanceLocation": "/foo", "errors": { "const": "Expected \"true\"" } } ] }, { "valid": true, "evaluationPath": "/else", "schemaLocation": "https://json-schema.fullstack.org.cn/blog/interpreting-output/example2#/else", "instanceLocation": "" } ]}
怎么会这样?
输出包含原因
通常,比实例通过验证的简单结果更重要的是它 *为什么* 通过验证,尤其是当它不是预期结果时。为了支持这一点,有必要包含所有相关的输出节点。
如果我们从结果中排除失败的输出节点,
{ "valid": true, "evaluationPath": "", "schemaLocation": "https://json-schema.fullstack.org.cn/blog/interpreting-output/example2#", "instanceLocation": "", "annotations": { "properties": [ "foo" ] }, "details": [ { "valid": true, "evaluationPath": "/properties/foo", "schemaLocation": "https://json-schema.fullstack.org.cn/blog/interpreting-output/example2#/properties/foo", "instanceLocation": "/foo" }, { "valid": true, "evaluationPath": "/else", "schemaLocation": "https://json-schema.fullstack.org.cn/blog/interpreting-output/example2#/else", "instanceLocation": "" } ]}
我们会发现 /else
子模式被评估,由此我们可以推断出 /if
子模式 *必须* 失败。但是,我们没有关于它 *为什么* 失败的信息,因为该子模式的输出被省略了。但是回顾完整的输出,很明显,/if
子模式失败是因为它期望 foo
为真。
出于这个原因,输出必须保留所有已评估子模式的节点。
还需要注意的是,规范 指出,if
关键字不会直接影响整体验证结果。
关于格式的说明
在我们结束之前,还有一个阅读输出的重要方面:格式。以上所有示例都使用 *分层* 格式(以前称为 *详细* 格式)。但是,根据您的需求和偏好,您可能希望使用 *列表* 格式(以前称为 *基本* 格式)。
以下是简单模式在 *列表* 格式中的输出
{ "valid": false, "details": [ { "valid": false, "evaluationPath": "", "schemaLocation": "https://json-schema.fullstack.org.cn/blog/interpreting-output/example1#", "instanceLocation": "" }, { "valid": true, "evaluationPath": "/properties/foo", "schemaLocation": "https://json-schema.fullstack.org.cn/blog/interpreting-output/example1#/properties/foo", "instanceLocation": "/foo" }, { "valid": false, "evaluationPath": "/properties/bar", "schemaLocation": "https://json-schema.fullstack.org.cn/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.fullstack.org.cn/blog/interpreting-output/example2#", "instanceLocation": "", "annotations": { "properties": [ "foo" ] } }, { "valid": true, "evaluationPath": "/properties/foo", "schemaLocation": "https://json-schema.fullstack.org.cn/blog/interpreting-output/example2#/properties/foo", "instanceLocation": "/foo" }, { "valid": false, "evaluationPath": "/if", "schemaLocation": "https://json-schema.fullstack.org.cn/blog/interpreting-output/example2#/if", "instanceLocation": "" }, { "valid": true, "evaluationPath": "/else", "schemaLocation": "https://json-schema.fullstack.org.cn/blog/interpreting-output/example2#/else", "instanceLocation": "" }, { "valid": false, "evaluationPath": "/if/properties/foo", "schemaLocation": "https://json-schema.fullstack.org.cn/blog/interpreting-output/example2#/if/properties/foo", "instanceLocation": "/foo", "errors": { "const": "Expected \"true\"" } } ]}
这里,很明显我们不能只扫描错误,因为我们必须考虑这些错误来自哪里。最后一个输出节点中的错误只与 /if
子模式有关,该子模式(如前所述)不会影响验证结果。
总结
JSON Schema 输出提供了您需要的所有信息,以便了解验证结果是什么以及评估器如何得出该结果。但是,要了解如何阅读它,需要了解所有部分存在的理由。
如果您有任何问题,请随时在我们 的 Slack 工作区(页脚中有链接)提问或 开启讨论。
所有输出都是使用我的在线评估器 https://json-everything.net/json-schema 生成的。
封面图片由 Tim Gouw 在 Unsplash 上提供