C++库json-schema-validator源码分析
最近框架接json schema的需求,因此对其进行了调研
star最多的两个库https://github.com/pboettch/json-schema-validator和https://github.com/danielaparker/jsoncons(已完成)
| json-schema-validator | jsoncons | |
|---|---|---|
| 规范支持 | Draft 7 | Draft 7,Draft 2019-09,Draft 2020-12 |
| 字符串格式检查器 | 没有预设,都要自己实现 | 支持date,email,tcp等常见的数十种 |
| 外部依赖 | C++11起,依赖github.com/nlohmann/json |
C++20起 |
| 更新频率 | 253 commits,7 months ago | 12335 commits,yesterday |
总体而言,jsoncons会更好(规范支持全面,功能多,更新频率高),但是接入难度更高(依赖C++20,框架要兼容C++11的钉子户用户)
json-schema-validator基本功能都有,凑活够用,所以还是先接入json-schema-validator,对其进行源码分析
这实际上也是对Draft 7标准的一个学习过程
分析是基于commit id 40af3ec39670e768fc3f01f935140af311d71024进行的
debug代码
由于存在很多递归调用,为了更好地分析代码运行流程,加一些dump堆栈的代码
1 |
|
然后是核心创建schema的代码,在创建开始的时候打印start和选中的json字段
1 | std::shared_ptr<schema> schema::make(json &schema, |
测试代码
测试的case如下
1 |
|
编译
1 | g++ -g -Wall -I ../tmp_install/include/ demo.cpp ../tmp_install/lib/libnlohmann_json_schema_validator.a -lunwind |
测试输出
输出去除了一些无用的堆栈内容
顶层person_schema
1 | ~ /a.out |grep -v 'shared_ptr\|c++/13\|libc-start\|_start' |
首先从main函数的set_root_schema开始,进入schema::make来创建顶层person_schema
会命中如下逻辑,也就是查找schema下面是否存在定义
1 | std::shared_ptr<schema> schema::make(json &schema, |
schema关键词definitions
每个定义的创建都是schema::make(def.value(), root, {defs, def.key()}, uris);
contact
1 | ============================================ |
对于定义contract,schema::make后进入了
(anonymous namespace)::type_schema::type_schema at /root/json-schema-validator/src/json-validator.cpp:638 (discriminator 2)
对应代码
1 | std::shared_ptr<schema> schema::make(json &schema, |
也就是说,如果schema::make的类型不是引用类型(找不到$ref),就type_schema::make创建type_schema类型的schema进行分析
type_schema::make又会进入schema::make逻辑,这是因为如下代码会对properties又创建一个schema进行分析:
1 | //在object的构造函数(anonymous namespace)::object::object at /root/json-schema-validator/src/json-validator.cpp:1145 |
name
输出更简单,因为没有properties类型,不会再创建schema
1 | ============================================ |
schema关键词properties
现在要分析顶层person_schema下的每个properties,正如上文所说
如果
schema::make的类型不是引用类型(找不到$ref),就type_schema::make创建type_schema类型的schema进行分析
type_schema::make又会进入schema::make逻辑,这是因为如下代码会对properties又创建一个schema进行分析:
1
2
3
4
5
6
7
8
9
10 //在object的构造函数(anonymous namespace)::object::object at /root/json-schema-validator/src/json-validator.cpp:1145
attr = sch.find("properties");
if (attr != sch.end()) {
for (auto prop : attr.value().items())
properties_.insert(
std::make_pair(
prop.key(),
schema::make(prop.value(), root, {"properties", prop.key()}, uris)));
sch.erase(attr);
}
顶层person_schema下存在3个properties
user
person_schema/properties/user作为type_schema创建以后,又触发了它的properties创建
也就是person_schema/properties/user/properties/contact和person_schema/properties/user/properties/name
1 | ============================================ |
每个user的子properties都有一个类似的堆栈
1 | (anonymous namespace)::schema::make at /root/json-schema-validator/src/json-validator.cpp:1390 (discriminator 9) |
这就是user的构造函数,对每个子properties都创建了一个type_schema,最后调用到schema::make
schema关键词ref
根据$ref引用已经存在的url元素
1 | std::shared_ptr<schema> schema::make(json &schema, |
不管是root->get_or_create_ref(),还是root->insert(),最后实际上都是调用了root->get_or_create_file()去在root的files_全局变量里面插入新的file
1 | schema_file &get_or_create_file(const std::string &loc) |
这就意味着root->insert(uri, root->get_or_create_ref(id)),这样插入一个空的占着位置也没事
后续解析到这个ref对应的uri,还会再插入一次root->insert(uri, std::make_shared<type_schema>(schema, root, uris))去覆盖
如果main函数里面validate的时候,还没有实际的type_schema,那就可以报错了
admin
1 | start|[{"properties":{"contact":{"$ref":"#/definitions/contact"},"name":{"$ref":"#/definitions/name"},"role":{"type":"string"}}},{"required":["name","contact","role"]},{"type":"object"}] |
guest
1 | ============================================ |
至此,整个person_schema解析完毕
解析完毕
输出,就是一开始顶层person_schema的部分内容,将整个person_schema插入到顶层uri#去
1 | [{"$schema":"http://json-schema.org/draft-07/schema#"},{"definitions":{"contact":{"properties":{"email":{"format":"email","type":"string"},"phone":{"type":"string"}},"required":["email"],"type":"object"},"name":{"description":"The person's name","type":"string"}}},{"properties":{"admin":{"properties":{"contact":{"$ref":"#/definitions/contact"},"name":{"$ref":"#/definitions/name"},"role":{"type":"string"}},"required":["name","contact","role"],"type":"object"},"guest":{"properties":{"contact":{"$ref":"#/definitions/contact"},"name":{"$ref":"#/definitions/name"},"visit_time":{"format":"date-time","type":"string"}},"required":["name","contact","visit_time"],"type":"object"},"user":{"properties":{"contact":{"$ref":"#/definitions/contact"},"name":{"$ref":"#/definitions/name"}},"required":["name","contact"],"type":"object"}}},{"title":"Person Schema"},{"type":"object"}]insert uri= # |
总结
这一篇分析比较粗糙,主要是跟着一个实际的解析例子走了一遍流程
递归流程虽然看起来复杂,实际上是有规律可循的:
definitions流程
在root的schema下的
schema::make中,创建出对应url的schema,给下一个properties流程引用properties流程
每个type object子字段的,都是会通过type_schema::make去创建出一个object
凡是object,会对
properties再递归调用type_schema::make去创建出一个object,也就是1
2
3
4
5(anonymous namespace)::object::object at /root/json-schema-validator/src/json-validator.cpp:1145
(anonymous namespace)::type_schema::make at /root/json-schema-validator/src/json-validator.cpp:1338 (discriminator 2)
(anonymous namespace)::type_schema::type_schema at /root/json-schema-validator/src/json-validator.cpp:638 (discriminator 2)
(anonymous namespace)::schema::make at /root/json-schema-validator/src/json-validator.cpp:1390 (discriminator 9)
(anonymous namespace)::object::object at /root/json-schema-validator/src/json-validator.cpp:1145所以root object会递归创建第一级的properties的各个object
而这每个object,又会对第二级properties再递归创建object
直到最低层级的叶子节点,变成了例如
"type": "string", "format": "email"的内容那么就不再创建object了,而是对其format去进行分析了
flowchart LR %% ───────── 入口 ───────── RootMake["schema::make (root)"] --> RootObj["object::object (root)"] %% ───────── 第 1 级 ───────── P1{{"properties\n(第1级)"}} RootObj --> P1 P1 -- 每个 key --> Obj1["object::object (第1级)"] %% ───────── 继续递归 / 叶子判定 ───────── IsLeaf{"是否叶子?<br>(type is object?)"} Obj1 --> IsLeaf IsLeaf -- 否 --> Pn{{"properties\n(...)"}} IsLeaf -- 是 --> Leaf["leaf node:\n type=string\n format=email"]