构建复杂模式
在编写中等复杂度的计算机程序时,人们普遍认为,将程序“构建”为可复用的方法比到处复制粘贴重复的代码要好。同样在 JSON Schema 中,对于除最琐碎的模式之外,构建在很多地方可以复用的模式非常有用。本章将介绍可用于复用和构建模式的工具以及使用这些工具的一些实例。
模式识别
与任何其他代码一样,将模式分解为在必要时相互引用的逻辑单元,则模式更易于维护。为了引用模式,我们需要一种识别模式的方法。模式文档由非相对 URI 所标识。
模式文档不需要有标识符,但如果您想从另一个模式引用一个模式,则需要一个标识符。在本文档中,我们将没有标识符的模式称为“匿名模式”。
在以下部分中,我们将看到如何确定模式的“标识符”。
笔记
URI 术语有时可能不直观。在本文件中,使用了以下定义:
URI [1]或 非相对 URI:含有模式的完整 URI(
https
)。它可能包含一个 URI 片段 (#foo
)。有时本文档会使用“非相对 URI”来明确表示不允许使用相对 URI。相对引用 [2]:不包含模式 (
https
) 的部分 URI 。它可能包含一个片段 (#foo
)。URI-引用 [3]:相对引用或非相对 URI。它可能包含一个 URI 片段 (
#foo
)。绝对 URI [4]包含模式 (
https
) 但不包含 URI 片段 (#foo
) 的完整 URI 。
笔记 尽管模式由 URI 标识,但这些标识符不一定是网络可寻址的。它们只是标识符。通常,实现不会发出 HTTP 请求 (
https://
) 或从文件系统 (file://
) 读取以获取模式。相反,它们提供了一种将模式加载到内部模式数据库中的方法。当模式被其 URI 标识符引用时,将从内部架构数据库中检索该模式。
JSON 指针
除了标识模式文档,您还可以标识子模式。最常见的方法是在指向子模式的 URI 片段中使用JSON 指针。
JSON 指针描述了一个以斜线分隔的路径来遍历文档中对象中的键。因此, /properties/street_address
意味着:
- 找到键的值
properties
- 在该对象中,找到键的值
street_address
URI https://example.com/schemas/address#/properties/street_address
标识以下模式中子模式 { "type": "string" }
。
{ "$id": "https://example.com/schemas/address", "type": "object", "properties": { "street_address": { "type": "string" }, "city": { "type": "string" }, "state": { "type": "string" } }, "required": ["street_address", "city", "state"]}
\$锚点
标识子模式的一种不太常见的方法是使用$anchor
关键字并在 URI 片段中使用该名称在模式中创建命名锚点。锚点必须以字母开头,后跟任意数量的字母、数字、-
、_
、:
、 或.
。
在 Draft 4 中,您以与 Draft 6-7 中相同的方式声明锚点,
$id
只是只是id
(没有美元符号)。
在 Draft 6-7 中,使用
$id
仅包含 URI 片段的定义了命名锚点。URI 片段的值是锚点的名称。JSON Schema 没有定义当
$id
同时包含片段和非片段 URI 部分时应该如何解析。因此,在设置命名锚点时,不应在 URI 引用中使用非片段 URI 部分。
笔记 如果一个命名的锚点在定义时不遵循这些命名规则,则它的行为未定义。您的锚点可能在某些实现中起作用,但在其他实现中不起作用。
URIhttps://example.com/schemas/address#street_address
标识以下模式的子模式
{ "$anchor": "#street_address", "type": "string" }
{ "$id": "https://example.com/schemas/address", "type": "object", "properties": { "street_address": { "$anchor": "#street_address", "type": "string" }, "city": { "type": "string" }, "state": { "type": "string" } }, "required": ["street_address", "city", "state"]}
基本 URI
使用非相对 URI 可能很麻烦,因此 JSON 模式中使用的任何 URI 都可以是 URI 引用,根据模式的基本 URI 进行解析,从而产生非相对 URI。本节介绍如何确定架构的基本 URI。
笔记 基本 URI 确定和相对引用解析由RFC-3986定义。如果您熟悉这在 HTML 中的工作原理,那么本节应该会感到非常熟悉。
检索 URI
用于获取模式的 URI 称为“检索 URI”。通常可以将匿名模式传递给实例,在这种情况下,该模式将没有检索 URI。
让我们假设使用 URI 引用 https://example.com/schemas/address
模式并检索以下模式。
{ "type": "object", "properties": { "street_address": { "type": "string" }, "city": { "type": "string" }, "state": { "type": "string" } }, "required": ["street_address", "city", "state"]}
此架构的基本 URI 与检索 URI 相同 https://example.com/schemas/address
。
\$id
您可以在模式根目录中使用$id
关键字来设置基本 URI 。$id
的值是一个没有根据检索 URI解析片段的 URI 引用。生成的 URI 是模式的基本 URI。
在Draft 4 中,
$id
只是id
(没有$)。
在Draft 4-7 中,允许在
$id
(或 Draft4 中的id
)中有片段。但是,设置包含 URI 片段的基本 URI 时的行为未定义,不应使用,因为实现可能会以不同方式对待它们。
笔记 当
$id
关键字出现在子模式中时,它的含义略有不同。有关更多信息,请参阅捆绑部分。
让我们假设 URIhttps://example.com/schema/address
和 https://example.com/schema/billing-address
两者都标识以下模式。
{ "$id": "/schemas/address", "type": "object", "properties": { "street_address": { "type": "string" }, "city": { "type": "string" }, "state": { "type": "string" } }, "required": ["street_address", "city", "state"]}
无论使用两个 URI 中的哪一个来检索此模式,基本 URI 都将是https://example.com/schemas/address
,这是$id
针对检索 URI的 URI 引用解析 的结果。
但是,在设置基本 URI 时使用相对引用可能会出现问题。例如,我们不能将此模式用作匿名模式,因为没有检索 URI并且您无法解析相对引用。出于这个原因和其他原因,建议您在使用$id
声明基本 URI 时尽量使用绝对 URI.
无论检索 URI是什么 或者它是否用作匿名模式,以下模式的基本 URI 将始终是 https://example.com/schemas/address 。
{ "$id": "https://example.com/schemas/address", "type": "object", "properties": { "street_address": { "type": "string" }, "city": { "type": "string" }, "state": { "type": "string" } }, "required": ["street_address", "city", "state"]}
\$ref
一个模式可以使用$ref
关键字引用另一个模式。$ref
的值是根据模式的Base URI解析的 URI 引用。当获取$ref
的值时,一个实现是使用解析的标识符来检索引用的模式并将该模式应用于实例中。
在Draft 4-7 中,
$ref
表现略有不同。当一个对象包含一个$ref
属性时,该对象被认为是一个引用,而不是一个模式。因此,您放入该对象的任何其他属性都不会被视为 JSON 模式关键字,并且会被验证器忽略。$ref
只能在需要模式的地方使用。
在这个例子中,假设我们要定义一个客户记录,其中每个客户可能都有一个送货地址和一个账单地址。地址总是相同的结构(有街道地址、城市和州),所以我们不想在存储地址的所有地方都存储同一个模式。这不仅会使模式更加冗长,而且会使将来更新变得更加困难。如果这个公司将来从事国际业务,想为所有地址添加一个国家/地区字段,那么最好在一个地方而不是在使用地址的所有地方进行此操作。
{
"$id": "https://example.com/schemas/customer",
"type": "object",
"properties": {
"first_name": { "type": "string" },
"last_name": { "type": "string" },
"shipping_address": { "$ref": "/schemas/address" },
"billing_address": { "$ref": "/schemas/address" }
},
"required": ["first_name", "last_name", "shipping_address", "billing_address"]
}
$ref
中的 URI 引用根据模式的基本 URI ( https://example.com/schemas/customer
)进行解析,结果为 https://example.com/schemas/address
. 该实现检索该模式并使用它来获取“shipping_address”和“billing_address”属性的值。
笔记
$ref
在匿名模式中使用时,相对引用可能无法解析。假设此示例用作匿名模式。
{
"type": "object",
"properties": {
"first_name": { "type": "string" },
"last_name": { "type": "string" },
"shipping_address": { "$ref": "https://example.com/schemas/address" },
"billing_address": { "$ref": "/schemas/address" }
},
"required": ["first_name", "last_name", "shipping_address", "billing_address"]
}
在/properties/shipping_address 的$ref 在没有非相对基础 URI 解析时解析是可以的,但 /properties/billing_address
中的$ref 无法解析到一个非相对 URI,因此无法用于检索 address 模式。
\$defs
有时,我们有一小段仅用于当前模式的子模式,将它们定义为单独的模式是没有意义的。虽然我们可以使用 JSON 指针或命名锚点来识别任何子模式,但$defs
关键字为我们提供了一个标准化的位置来保存想在当前模式文档中复用的子模式。
让我们扩展之前的客户模式示例,以使用名称属性的通用架构。为此定义一个新模式没有意义,它只会在这个模式中使用,所以使用$defs
非常合适。
{
"$id": "https://example.com/schemas/customer",
"type": "object",
"properties": {
"first_name": { "$ref": "#/$defs/name" },
"last_name": { "$ref": "#/$defs/name" },
"shipping_address": { "$ref": "/schemas/address" },
"billing_address": { "$ref": "/schemas/address" }
},
"required": ["first_name", "last_name", "shipping_address", "billing_address"],
"$defs": {
"name": { "type": "string" }
}
}
$ref
不仅有助于避免重复。它对于编写更易于阅读和维护的模式也很有用。模式的复杂部分可以$defs
用描述性名称定义并在需要的地方引用。这允许模式的读者在深入研究更复杂的部分之前,更快速、更轻松地在高层次上理解模式。
笔记 可以引用外部子模式,但通常您希望将 a 限制
$ref
为引用外部模式或$defs
.
递归
该$ref
关键字可以被用来创建一个自我递归模式。例如,您可能有一个person
模式包含一个 children 的数组,每个children
也是person
的实例。
{
"type": "object",
"properties": {
"name": { "type": "string" },
"children": {
"type": "array",
"items": { "$ref": "#" }
}
}
}
英国王室的家庭树片段
{
"name": "Elizabeth",
"children": [
{
"name": "Charles",
"children": [
{
"name": "William",
"children": [
{ "name": "George" },
{ "name": "Charlotte" }
]
},
{
"name": "Harry"
}
]
}
]
}
上面,我们创建了一个引用自身的模式,有效地在验证器中创建了一个“循环”,这是合法且有用的。但是请注意,$ref
对另一个$ref
的引用可能会导致解析器中的无限循环,这是明确禁止的。
{
"$defs": {
"alice": { "$ref": "#/$defs/bob" },
"bob": { "$ref": "#/$defs/alice" }
}
}
扩展递归模式
2019-09 Draft 中的新内容文档即将推出
捆绑
使用多个模式文档便于开发,但将所有模式捆绑到单个模式文档中通常更方便分发。这可以通过在子模式中使用$id 关键字来完成 。当$id
在子模式中使用时,它表示嵌入式模式。
嵌入式模式的标识符是根据它出现在其中的模式的基本 URI 解析的得到的$id
的值。包含嵌入模式的模式文档称为复合模式文档,复合架构文档中每个带有$id 的模式称为模式资源。
Draft 4 中,
$id
只是id
(没有$)。
Draft 4-7 中,子模式中的$id 不表示嵌入式模式。相反,它只是单模式文档中的基本 URI 更改。
这类似于 HTML 中的
<iframe>
的标签。
笔记 在开发模式时使用嵌入式模式是不常见的。通常最好不要显式使用此功能,并在需要时使用模式捆绑工具来构建捆绑模式。
此示例显示捆绑到复合模式文档中的客户模式示例和地址模式示例。
{
"$id": "https://example.com/schemas/customer",
"$schema": "https://json-schema.org/draft/2019-09/schema",
"type": "object",
"properties": {
"first_name": { "type": "string" },
"last_name": { "type": "string" },
"shipping_address": { "$ref": "/schemas/address" },
"billing_address": { "$ref": "/schemas/address" }
},
"required": ["first_name", "last_name", "shipping_address", "billing_address"],
"$defs": {
"address": {
"$id": "/schemas/address",
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"street_address": { "type": "string" },
"city": { "type": "string" },
"state": { "$ref": "#/definitions/state" }
},
"required": ["street_address", "city", "state"],
"definitions": {
"state": { "enum": ["CA", "NY", "... etc ..."] }
}
}
}
}
无论是否捆绑了模式资源,复合模式文档中的所有引用都必须相同。请注意,$ref
客户架构中的 关键字没有更改。唯一的区别是地址模式现在定义在 /$defs/address
而不是单独的模式文档。您不能使用#/$defs/address
引用地址架构,因为如果您拆分模式,该引用将不再指向地址模式。
Draft 4-7 中,这两个 URI 都是有效的,因为子模式
$id
仅表示基本 URI 更改,而不是嵌入模式。但是,虽然允许,仍然强烈建议 JSON 指针不要越过具有基本 URI 更改的模式。
您还应该看到"$ref": "#/definitions/state"解析为地址模式中的 definitions 关键字,而不是顶级模式中的关键字,就像未使用嵌入式模式时那样。
每个模式资源都是独立求值的,并且可能使用不同的 JSON 模式 dialect。上面的示例中, 地址模式资源使用了 Draft 7 ,而客户模式资源使用 Draft 2019-09。如果嵌入式模式中没有声明$schema
,则默认使用父模式的 dialect。
Draft 4-7 中,子
$id
模式只是基本 URI 更改,不被视为独立的模式资源。因为$schema
仅允许在模式资源的根目录中使用,所以使用子模式$id
捆绑的所有模式必须使用相同的 dialect。