0%

JSON Schema笔记:复用

为什么要复用?

复用通常带来两个好处:

  • 简洁
  • 方便修改

如果某些属性节点的结构都是一样的,比如地址,公司地址、家庭地址等基本都是相同的描述方式,省、市、区……因此,我们总不希望在编写 JSON Schema 时到处复制/粘贴吧。

通常,这种会被多处使用的相同结构,我们总是想要在一处定义,在使用的地方引用。这不仅显得简洁,而且如果需要修改,也只需要修改定义之处即可。

有人可能会认为“复用”只是让 JSON Schema 显得更加“优雅”,但这并非必须的,不复用大不了再写一遍呗!

But,某些情况下,如果不使用引用复用,那么可能无法定义某些节点。典型的场景之一是——递归结构——使用直接定义的方式穷尽所有层级是不可能的。

如何复用?

文档内引用

通过属性路径引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
"$schema": "http://json-schema.org/draft-07/schema#",

"definitions": {
"address": {
"type": "object",
"properties": {
"street_address": { "type": "string" },
"city": { "type": "string" },
"state": { "type": "string" }
},
"required": ["street_address", "city", "state"]
}
},

"type": "object",

"properties": {
"billing_address": { "$ref": "#/definitions/address" },
"shipping_address": { "$ref": "#/definitions/address" }
}
}

以上是官方文档给出的一个示例。从示例可见,该 JSON Schema 定义的数据包含了两种地址属性,即 billing_addressshipping_address,它们均引用了 address 定义的模式结构。

由此可见,复用其实分为两步:定义模式结构及引用定义的模式结构。

首先,我们结合示例来看第一步——模式结构的定义。本质上,这没有什么特别的,就是一个子模式,更直白地说,就是一个普通的属性定义。

惯例上,通常把模式结构定义置于名为 definitions 父模式下——示例里也是这样做的——但这只是一种惯例,不是强制的规范约束。

有了定义,第二步就是通过引用达到复用的目的了。示例中,使用 $ref 关键字来引用定义,$ref 的值是一个 JSON Pointer(详细的语法参见这里)——它指示了引用的结点路径。

# 指当前整个文档,/ 是路径分隔,因此 #/definitions/address 即指从当前文档根结点开始,引用 definitions 下的 address 模式定义。

如同 XPointer 定位 XML 文档一样,JSON Pointer 定位 JSON 文档。

对于引用属性而言,它应是一个仅包含$ref 属性的对象,如果除 $ref 外还有其他属性,将被校验器(validator)忽略。

通过$id引用

上面我们其实是使用的属性路径来指定的引用,而 $id 可以定义唯一标识,因此当然也可以通过 $id 引用。比如上面的例子可以改写为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"$schema": "http://json-schema.org/draft-07/schema#",

"definitions": {
"address": {
"$id": "#address",
"type": "object",
"properties": {
"street_address": { "type": "string" },
"city": { "type": "string" },
"state": { "type": "string" }
},
"required": ["street_address", "city", "state"]
}
},

"type": "object",

"properties": {
"billing_address": { "$ref": "#address" },
"shipping_address": { "$ref": "#address" }
}
}

注意:Python jsonschema 库不支持该功能。

跨文档引用

显而易见,上例中引用的是本文档内的定义。但是,有时为了降低复杂度,可能会拆分为多个文档。这时,就需要像下面这样引用另一文档中的模式定义:

1
{ "$ref": "definitions.json#/address" }

需要注意的是,这样写假定了两个前提:

  • 引用文档与当前文档在同一路径下
  • 当前文档未指定 $id

这里涉及 $id 的另一个用途,它会影响 $ref 中 URI 的解析。

比如,当前文档定义了 "{ $id": "http://foo.bar/schemas/person.json" },又引用 { "$ref": "address.json" },那么 address.json 的 URI 将被解析为 http://foo.bar/schemas/address.json

如果引用的不是文档中整个模式定义,那么还需要在文档中定位,这跟在本文档中定位类似:

1
{ "$ref": "definitions.json#/address" }

理论上说,$ref 可以是一个 URI 引用,因此需要解析访问。但是,JSON Schema 规范草案并没有明确 URI 的处理行为,这取决于校验器的实现。
因此,不应假设校验器会解析网络资源,除非所使用的库明确实现了该行为。

小结

总结下 $id 的几个作用:

  • 唯一标识符;
  • 作为唯一标识符被 $ref 引用;
  • 作为基 URI 影响 $ref 中外链 URI 的解析。

需要注意的是,在实际应用中,涉及引用时(即上述后两种用法),需要确认所使用的库是否实现。