2023 年 11 月 8 日,星期三 ·7分钟阅读

解读 JSON Schema 输出

在过去几年里,我收到了很多关于 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,以下是一个通过验证的实例:

data
{ "foo": true, "bar": 1 }

输出为

data
{ "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 是一个字符串)

data
{ "foo": true, "bar": "value" }

输出为

data
{ "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 的子模式输出无效,根节点也无效,虽然我们可能不太高兴因为它失败了,但至少我们知道为什么。

那么情况总是这样吗?通过验证的子模式可能会有失败的子模式吗?绝对有可能!

更多复杂性

我们可以创建无限种模式和实例,这些模式和实例可以同时通过验证,并输出失败的节点。几乎所有这些都与提供多种选择的关键字有关(anyOfoneOf)或条件语句(ifthenelse)。这些情况,特别是,具有旨在失败但仍然产生成功验证结果的子模式。

对于这篇文章,我将重点关注以下条件模式,但相同的想法也适用于包含“多个选项”关键字的模式。

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 属性。因此,以下两个都是有效的

data
{ "foo": true, "bar": 1 }
data
{ "foo": false, "baz": 1 }

当我们查看第一个实例的验证输出时,我们得到的输出类似于上一节中的快乐路径:所有输出节点都具有 valid: true,一切都有意义。

但是,查看第二个实例的验证输出(如下),我们注意到 /if 子模式的输出节点具有 valid: false。但是,总体的验证通过了。

data
{ "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": "" } ]}

怎么会这样?

输出包含原因

通常,比实例通过验证的简单结果更重要的是它 *为什么* 通过验证,尤其是当它不是预期结果时。为了支持这一点,有必要包含所有相关的输出节点。

如果我们从结果中排除失败的输出节点,

data
{ "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 关键字不会直接影响整体验证结果。

关于格式的说明

在我们结束之前,还有一个阅读输出的重要方面:格式。以上所有示例都使用 *分层* 格式(以前称为 *详细* 格式)。但是,根据您的需求和偏好,您可能希望使用 *列表* 格式(以前称为 *基本* 格式)。

以下是简单模式在 *列表* 格式中的输出

data
{ "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 中的节点,查看是否存在任何包含错误的节点。

以下是条件模式在 *列表* 格式中的输出

data
{ "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 GouwUnsplash 上提供