Python-jsonschema 中的 JSON Schema 引用机制详解
理解 JSON Schema 引用机制
在 JSON Schema 规范中,$ref
和 $dynamicRef
关键字允许模式作者组合多个模式(或子模式)以实现复用或去重。这种引用机制是构建复杂、模块化 JSON Schema 的基础。
引用机制的核心组件
Python-jsonschema 项目使用独立的 referencing
库来实现引用解析功能,这个库提供了三个核心对象:
- Registry:表示一组不可变的 JSON Schema 集合(可以是内存中的或可检索的)
- Specification:表示 JSON Schema 规范的特定版本(不同版本可能有不同的引用行为)
- Resource:表示特定的 JSON Schema(通常是 Python 字典)及其关联的 Specification
资源(Resource)的重要性
一个 JSON Schema 可能被不同版本的规范解释。例如,简单的模式 {"type": "integer"}
:
- 在 Draft 2020-12 下,
2.0
被视为整数 - 在 Draft 4 下,
2.0
可能不被视为整数
因此,创建 Resource 时需要明确指定其适用的规范版本:
from referencing import Resource
from referencing.jsonschema import DRAFT202012
# 为 Draft 2020-12 创建资源
resource = Resource(contents={"type": "integer"}, specification=DRAFT202012)
配置引用行为的实践指南
配置 jsonschema 的引用行为主要分为两步:
- 创建符合需求的
referencing.Registry
对象 - 在实例化
Validator
时传递该 Registry
场景一:添加内存中的模式
这是最常见的场景,我们需要让一些内存中的模式可用于验证过程。
from referencing import Registry, Resource
# 定义非负整数模式
schema = Resource.from_contents({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "integer",
"minimum": 0
})
# 创建 Registry 并添加资源
registry = Registry().with_resources([
("http://example.com/nonneg-int-schema", schema),
("urn:nonneg-integer-schema", schema),
])
# 使用自定义 Registry 创建验证器
from jsonschema import Draft202012Validator
validator = Draft202012Validator(
{
"type": "object",
"additionalProperties": {"$ref": "urn:nonneg-integer-schema"},
},
registry=registry,
)
场景二:从文件系统解析引用
如果需要动态从文件系统加载模式,可以配置一个检索函数:
from pathlib import Path
import json
from referencing import Registry, Resource
from referencing.exceptions import NoSuchResource
SCHEMAS = Path("/tmp/schemas")
def retrieve_from_filesystem(uri: str):
if not uri.startswith("http://localhost/"):
raise NoSuchResource(ref=uri)
path = SCHEMAS / Path(uri.removeprefix("http://localhost/"))
contents = json.loads(path.read_text())
return Resource.from_contents(contents)
registry = Registry(retrieve=retrieve_from_filesystem)
场景三:支持 YAML 模式
通过自定义检索函数,我们可以支持 YAML 格式的模式:
from pathlib import Path
import yaml
from referencing import Registry, Resource
from referencing.exceptions import NoSuchResource
SCHEMAS = Path("/tmp/yaml-schemas")
def retrieve_yaml(uri: str):
if not uri.startswith("http://localhost/"):
raise NoSuchResource(ref=uri)
path = SCHEMAS / Path(uri.removeprefix("http://localhost/"))
contents = yaml.safe_load(path.read_text())
return Resource.from_contents(contents)
registry = Registry(retrieve=retrieve_yaml)
注意:YAML 不是所有特性都兼容 JSON 数据模型,应确保使用的 YAML 子集可以无损转换为 JSON。
场景四:通过 HTTP 自动检索资源
虽然 JSON Schema 规范不鼓励自动网络检索,但在受控环境下可以实现:
from referencing import Registry, Resource
import httpx
def retrieve_via_httpx(uri: str):
response = httpx.get(uri)
return Resource.from_contents(response.json())
registry = Registry(retrieve=retrieve_via_httpx)
安全警告:这种配置有潜在安全风险,应确保理解并接受其影响。
从旧版 RefResolver 迁移
旧版 jsonschema 使用 _RefResolver
进行引用解析,现已弃用。迁移指南:
store 参数替代
旧代码:
from jsonschema import Draft202012Validator, RefResolver
resolver = RefResolver.from_schema(
schema={"title": "my schema"},
store={"http://example.com": {"type": "integer"}},
)
新代码:
from referencing import Registry
from referencing.jsonschema import DRAFT202012
registry = Registry().with_resource(
"http://example.com",
DRAFT202012.create_resource({"type": "integer"}),
)
处理程序替代
旧版的 handlers
功能可以通过自定义 retrieve
函数实现:
from urllib.parse import urlsplit
def retrieve(uri: str):
parsed = urlsplit(uri)
if parsed.scheme == "file":
# 处理文件协议
...
elif parsed.scheme == "custom":
# 处理自定义协议
...
最佳实践建议
- 明确指定 $schema:在模式中使用
$schema
关键字消除版本歧义 - 优先使用内存模式:对于固定模式,预加载到内存更高效
- 谨慎使用网络检索:仅在受控环境中使用自动网络检索
- 考虑性能影响:频繁的文件系统或网络访问会影响验证性能
- 测试边缘情况:特别是跨版本引用和循环引用情况
通过合理配置引用机制,可以构建出既灵活又强大的 JSON Schema 验证系统。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考