为什么要复用?
复用通常带来两个好处:
- 简洁
- 方便修改
如果某些属性节点的结构都是一样的,比如地址,公司地址、家庭地址等基本都是相同的描述方式,省、市、区……因此,我们总不希望在编写 JSON Schema 时到处复制/粘贴吧。
通常,这种会被多处使用的相同结构,我们总是想要在一处定义,在使用的地方引用。这不仅显得简洁,而且如果需要修改,也只需要修改定义之处即可。
有人可能会认为“复用”只是让 JSON Schema 显得更加“优雅”,但这并非必须的,不复用大不了再写一遍呗!
But,某些情况下,如果不使用引用复用,那么可能无法定义某些节点。典型的场景之一是——递归结构——使用直接定义的方式穷尽所有层级是不可能的。
如何复用?
文档内引用
通过属性路径引用
1 | { |
以上是官方文档给出的一个示例。从示例可见,该 JSON Schema 定义的数据包含了两种地址属性,即 billing_address
和 shipping_address
,它们均引用了 address
定义的模式结构。
由此可见,复用其实分为两步:定义模式结构及引用定义的模式结构。
首先,我们结合示例来看第一步——模式结构的定义。本质上,这没有什么特别的,就是一个子模式,更直白地说,就是一个普通的属性定义。
惯例上,通常把模式结构定义置于名为 definitions
父模式下——示例里也是这样做的——但这只是一种惯例,不是强制的规范约束。
有了定义,第二步就是通过引用达到复用的目的了。示例中,使用 $ref
关键字来引用定义,$ref
的值是一个 JSON Pointer(详细的语法参见这里)——它指示了引用的结点路径。
#
指当前整个文档,/
是路径分隔,因此 #/definitions/address
即指从当前文档根结点开始,引用 definitions
下的 address
模式定义。
如同 XPointer 定位 XML 文档一样,JSON Pointer 定位 JSON 文档。
对于引用属性而言,它应是一个仅包含$ref
属性的对象,如果除 $ref
外还有其他属性,将被校验器(validator)忽略。
通过$id引用
上面我们其实是使用的属性路径来指定的引用,而 $id
可以定义唯一标识,因此当然也可以通过 $id
引用。比如上面的例子可以改写为:
1 | { |
注意: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 的解析。
需要注意的是,在实际应用中,涉及引用时(即上述后两种用法),需要确认所使用的库是否实现。