2022 年 3 月 21 日,星期一 ·16分钟阅读

一切从适用性开始 - JSON Schema 基础知识第一部分

"验证从将根模式应用于完整的实例文档开始。应用关键字将子模式应用于实例位置。" - 摘自 JSON Schema 在 5 分钟内

JSON Schema 的主要用例是验证。因此,理解验证过程是如何发生的至关重要。让我们花点时间来正确理解 JSON Schema 的基本概念“适用性”。

应用关键字

JSON Schema 由许多关键字组成。这些关键字可以细分为类别,其中之一是“应用”。在物理意义上,“应用器”是你用来将一种物质引入另一种物质的东西。例如,一块布可以用来将抛光剂引入一张漂亮的木桌。布是应用器。抛光剂通过布应用于桌子。

JSON Schema 中的应用关键字类似于布,但它们将模式应用于实例数据中的位置(或简称为“实例位置”)。

从一切开始

JSON Schema 的验证过程从将整个 JSON Schema 应用于整个实例开始。此应用(模式到实例)的结果应产生一个布尔断言,即验证结果。

JSON Schema 可以是布尔值或对象。在上面提到的介绍性文章中,我们注意到如何将 truefalse 的布尔模式应用到实例数据,无论实例数据是什么,都会产生相同的断言结果(分别为真和假)。我们还注意到,等效的对象模式分别是 { }{ "not": { } }。(not 关键字会反转断言结果。)

词汇检查

"断言"是一个事实陈述。这用于指代计算中测试的结果。测试可以称为“X 是 1”。如果测试通过,则断言为真!

当我们谈论整个模式在应用方面的意义时,我们通常将其称为“根模式”。这是因为应用于特定实例位置的其他模式是不同的,我们称它们为“子模式”。区分根模式和子模式使我们能够清楚地沟通我们正在谈论哪个 JSON Schema,以及何时将 Schema 用作验证过程的一部分。

以下示例假定使用的是 JSON Schema 2020-12。如果需要了解有关早期版本(或草案)JSON Schema 的内容,将进行突出显示。

子模式应用 - 验证对象和数组

如果你的 JSON 实例是对象或数组,你可能希望验证对象的 value 或数组中的 item。在本介绍中,你将使用 propertiesitems 关键字以及子模式。

验证对象

让我们来看一个例子。这是我们的实例数据。

数据
{ "id": 1234, "name": "Bob", "email": "[email protected]", "isEmailConfirmed": true}

要创建模式的基础,我们将复制该结构并将其放置在 properties 关键字下,将 value 更改为空对象,然后定义类型。

数据
{ "properties": { "id": { "type": "number" }, "name": { "type": "string" }, "email": { "type": "string" }, "isEmailConfirmed": { "type": "boolean" } }}

的值必须为一个对象,并且该对象的各值必须为模式。这些模式为子模式。

好的,让我们检查一下我们的模式是否满足了我们的所有需求。例如,当我们的实例缺少字段时会发生什么?验证仍然通过。这是因为子模式应用于实例值,仅当键匹配时。

数据
{ "id": 1234, "name": "Bob", "email": "[email protected]", "isEmailConfirmed": "true"}// isEmailConfirmed 应该是一个布尔值,而不是一个字符串。// 将导致验证错误。

我们需要确保如果我们希望对象中的任何键是必需的,我们就定义了适当的约束。我们可以通过将关键字添加到我们的模式来实现这一点。

数据
{ "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 的值为 STRING 而不是 Boolean,但验证仍然通过。仔细观察,你会发现键的拼写错误为 "isEmaleConfirmed"。谁知道为什么,但事实就是这样。

幸运的是,使用我们的 Schema 来解决这个问题很简单。 additionalProperties 关键字允许你防止在对象中使用除了在 properties 中定义的属性(或键)之外的属性。

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

additionalProperties 的值不仅仅是布尔值,而是一个模式。此子模式值将应用于示例中 properties 对象中未定义的所有实例对象的值。您可以使用 additionalProperties 允许额外的属性,但将其值限制为字符串。

这里做了一些简化,以便我们更好地理解想要学习的概念。如果您想更深入地了解,请查看我们的 关于 additionalProperties 的学习资源

最后,如果我们期望一个对象,但得到的是一个数组或其他非对象类型呢?

数据
[ { "id": 1234, "name": "Bob", "email": "[email protected]", "isEmaleConfirmed": "true" }]// 数组不是对象...

你可能会惊讶于这居然能通过验证!但为什么呢!?

我们目前探索的三个关键词,propertiesrequired,以及 additionalProperties 仅定义对对象的约束,在遇到其他类型时会被忽略。如果我们想确保类型符合预期(一个对象),我们需要明确指定这个约束!

数据
{ "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 运算符进行组合。如果任何一个断言结果是 true,则 anyOf 返回 true 断言。如果所有断言结果都是 false,则 anyOf 返回 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 呢?该关键字使用的布尔逻辑是一种“异或”... 算是吧。“异或”通常用于电子学,但不能完全等同于“只有一个可以为真”,而这正是 JSON Schema 中 oneOf 的意图。

这是两个输入的真值表(如果 oneOf 的数组值只包含两个子模式值)。

异或的真值表

看起来没问题,对吧?但是如果我们添加另一个“输入”,使其数量为奇数呢?

具有三个输入的异或的真值表

看起来“大部分”是正确的,但请注意,如果所有断言都是 true,那么最终断言也为 `true`!这不是我们想要的结果,但这是数学上正确的结果。因此,我们必须扩展逻辑定义以包含“... 且 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 上拍摄