RapidJSON JSON Schema 校验完全指南:SchemaDocument 编译、SAX 流式验证、远程引用与违规报告
【免费下载链接】rapidjsonA fast JSON parser/generator for C++ with both SAX/DOM style API项目地址: https://gitcode.com/GitHub_Trending/ra/rapidjson
本篇指南基于 RapidJSON 官方文档 schema.md 与仓库源码展开,系统讲解 RapidJSON 的 JSON Schema 校验能力:如何将 Schema 编译为SchemaDocument、如何用SchemaValidator在 DOM/SAX 解析与序列化过程中即时验证、如何通过IRemoteSchemaDocumentProvider解析远程$ref,以及完整读取结构化违规报告。读完本文,你可以把 Draft-04 规范的 JSON Schema 校验无缝集成到 C++ 解析管线中,并理解其底层实现与性能特征。
功能定位:为什么需要 Schema 校验
JSON Schema 是一套描述 JSON 数据格式的标准草案,Schema 本身也是 JSON 数据。RapidJSON 实现了针对 JSON Schema Draft v4 规范的校验器(该功能自 v1.1.0 发布)。其价值在于两点:
- 安全访问 DOM:先用 Schema 验证 JSON 结构,之后代码就可以放心地取类型、取成员,不必到处手写
IsObject()、HasMember()之类的防御性检查; - 保证序列化合规:在写出 JSON 之前先过一遍 Schema,确保输出结果一定符合约定格式。
核心头文件是 include/rapidjson/schema.h(约 3200 行),涉及三个关键类型:
| 类型 | 源码位置 | 职责 |
|---|---|---|
SchemaDocument | schema.h | 编译后的 Schema,可被多个校验器共享,校验过程中不被修改 |
SchemaValidator | schema.h#L3178 | SAX Handler,接收 SAX 事件并即时判定合法性 |
SchemaValidatingReader | schema.h#L3199 | 组合 Reader + Validator + Document 的辅助类 |
从源码看,SchemaValidator实际是GenericSchemaValidator<SchemaDocument>的 typedef(typedef GenericSchemaValidator<SchemaDocument> SchemaValidator;),而GenericSchemaValidator继承 SAX handler 接口并额外提供Reset()、IsValid()、GetError()、GetInvalidSchemaPointer()等方法。
基本用法:编译 Schema 并验证 Document
标准流程分三步:把 Schema JSON 解析为Document;将其编译为SchemaDocument;构造SchemaValidator并用document.Accept(validator)触发 SAX 事件流完成验证。
#include "rapidjson/schema.h" // ... Document sd; if (sd.Parse(schemaJson).HasParseError()) { // the schema is not a valid JSON. // ... } SchemaDocument schema(sd); // Compile a Document to SchemaDocument if (!schema.GetError().ObjectEmpty()) { // there was a problem compiling the schema StringBuffer sb; Writer<StringBuffer> w(sb); schema.GetError().Accept(w); printf("Invalid schema: %s\n", sb.GetString()); } // sd is no longer needed here. Document d; if (d.Parse(inputJson).HasParseError()) { // the input is not a valid JSON. // ... } SchemaValidator validator(schema); if (!d.Accept(validator)) { // Input JSON is invalid according to the schema // Output diagnostic information StringBuffer sb; validator.GetInvalidSchemaPointer().StringifyUriFragment(sb); printf("Invalid schema: %s\n", sb.GetString()); printf("Invalid keyword: %s\n", validator.GetInvalidSchemaKeyword()); sb.Clear(); validator.GetInvalidDocumentPointer().StringifyUriFragment(sb); printf("Invalid document: %s\n", sb.GetString()); }两个重要的复用规则:
- 一个
SchemaDocument可被多个SchemaValidator共享引用,且不会被校验器修改——因此"编译一次、验证多次"是推荐用法; SchemaValidator本身也可复用,验证下一个文档前调用validator.Reset()即可。从源码看,Reset()会把校验器内部的上下文栈回卷到初始状态并清空错误对象,避免重新分配内存。
GetInvalidSchemaPointer()/GetInvalidDocumentPointer()返回的都是 JSON Pointer 类型(GenericPointer),StringifyUriFragment()将其序列化为#/a/b/0形式的 URI fragment。
解析/序列化过程中的即时校验(Fused Validation)
与大多数 JSON Schema 校验实现"先解析成树、再遍历验证"不同,RapidJSON 的校验器是SAX-based的:可以直接从流中边解析边验证。一旦发现某个 JSON 值违反 Schema,解析会立即终止,不再继续读入后续内容——这对解析大型 JSON 文件尤其有用。
DOM 解析模式
DOM 模式要求Document在接收 SAX 事件之外还要做构建/收尾工作,因此需要SchemaValidatingReader来同时路由 Reader、Validator 和 Document 三者:
#include "rapidjson/filereadstream.h" // ... SchemaDocument schema(sd); // Compile a Document to SchemaDocument // Use reader to parse the JSON FILE* fp = fopen("big.json", "r"); FileReadStream is(fp, buffer, sizeof(buffer)); // Parse JSON from reader, validate the SAX events, and store in d. Document d; SchemaValidatingReader<kParseDefaultFlags, FileReadStream, UTF8<> > reader(is, schema); d.Populate(reader); if (!reader.GetParseResult()) { // Not a valid JSON // When reader.GetParseResult().Code() == kParseErrorTermination, // it may be terminated by: // (1) the validator found that the JSON is invalid according to schema; or // (2) the input stream has I/O error. // Check the validation result if (!reader.IsValid()) { // Input JSON is invalid according to the schema // Output diagnostic information StringBuffer sb; reader.GetInvalidSchemaPointer().StringifyUriFragment(sb); printf("Invalid schema: %s\n", sb.GetString()); printf("Invalid keyword: %s\n", reader.GetInvalidSchemaKeyword()); sb.Clear(); reader.GetInvalidDocumentPointer().StringifyUriFragment(sb); printf("Invalid document: %s\n", sb.GetString()); } }从源码看,SchemaValidatingReader内部持有一个GenericSchemaValidator,在每次 SAX 回调失败时把解析错误码置为kParseErrorTermination,并把指针位置、错误关键词、错误码和完整错误对象拷贝到自身(invalidSchemaPointer_、error_等成员),所以验证失败后可以从 reader 上直接读取全部诊断信息,无需再接触内部校验器。
SAX 解析模式
如果只需要验证而不需要进一步处理,这是最简单的形式:
SchemaValidator validator(schema); Reader reader; if (!reader.Parse(stream, validator)) { if (!validator.IsValid()) { // ... } }这正是示例程序 example/schemavalidator/schemavalidator.cpp 采用的方式。其显著优势是内存占用极低,与 JSON 文件大小无关(内存用量只取决于 Schema 的复杂度)。
该示例还展示了完整的命令行用法:从文件读入 Schema 编译为SchemaDocument,再用FileReadStream(stdin, ...)包裹标准输入,调用reader.Parse(is, validator)流式校验;若reader.GetParseErrorCode() == kParseErrorTermination,说明是校验器主动终止而非语法错误,随后通过validator.GetError()拿到完整报告,并用GetValidateError_En()把错误码翻译为英文消息(见 include/rapidjson/error/en.h)。
如果还需要继续处理 SAX 事件(例如边验证边转发给其他 Handler),则要用模板类显式指定输出 handler:
MyHandler handler; GenericSchemaValidator<SchemaDocument, MyHandler> validator(schema, handler); Reader reader; if (!reader.Parse(ss, validator)) { if (!validator.IsValid()) { // ... } }序列化时的校验
也可以反过来在写出 JSON 的过程中做验证,确保序列化结果符合 Schema:
StringBuffer sb; Writer<StringBuffer> writer(sb); GenericSchemaValidator<SchemaDocument, Writer<StringBuffer> > validator(s, writer); if (!d.Accept(validator)) { // Some problem during Accept(), it may be validation or encoding issues. if (!validator.IsValid()) { // ... } }GenericSchemaValidator会把每个 SAX 事件同时分发给校验逻辑和下游Writer。如果应用本身只需要 SAX 风格的序列化,也可以直接把事件发给SchemaValidator而不经Writer。
远程 Schema:IRemoteSchemaDocumentProvider 与 $ref
JSON Schema 支持$ref关键字,它是一个 JSON Pointer,可以引用本地或远程 Schema:本地引用以#为前缀,远程引用则是相对或绝对 URI,例如:
{ "$ref": "definitions.json#/address" }SchemaDocument自己并不知道如何解析这种 URI,需要用户提供一个IRemoteSchemaDocumentProvider实例来完成解析:
class MyRemoteSchemaDocumentProvider : public IRemoteSchemaDocumentProvider { public: virtual const SchemaDocument* GetRemoteDocument(const char* uri, SizeType length) { // Resolve the uri and returns a pointer to that schema. } }; // ... MyRemoteSchemaDocumentProvider provider; SchemaDocument schema(sd, &provider);从源码结构看,GenericSchemaDocument构造函数签名为GenericSchemaDocument(const ValueType& document, const Ch* uri = 0, SizeType uriLength = 0, IRemoteSchemaDocumentProviderType* remoteProvider = 0, Allocator* allocator = 0, const PointerType& pointer = PointerType(), const Specification& spec = Specification(kDraft04))——除了远程 provider 外,还可以传入 Schema 的 base URI(用于违规报告中的schemaRef定位)、独立 allocator 和起始 JSON Pointer(用于只编译大文档中的某个子 Schema),规范版本参数默认 Draft-04,并可自动识别文档根部的$schema/swagger/openapi字段。
规范符合性(Conformance)
RapidJSON 在 JSON Schema Test Suite(Draft-4 部分)中通过了263 个测试中的 262 个。唯一的失败用例是refRemote.json中 "change resolution scope" 的 "changed scope ref invalid",原因是id关键字与 URI 组合功能尚未实现。
另外两点注意:
- 字符串的
format关键字被忽略,因为规范并未要求实现它; pattern与patternProperties依赖正则表达式,默认使用 RapidJSON 自研的 NFA 正则引擎(include/rapidjson/internal/regex.h)。
内置正则引擎支持的语法
| Syntax | Description |
|---|---|
ab | 串联(Concatenation) |
a\|b | 选择(Alternation) |
a? | 0 次或 1 次 |
a* | 0 次或多次 |
a+ | 1 次或多次 |
a{3} | 恰好 3 次 |
a{3,} | 至少 3 次 |
a{3,5} | 3 到 5 次 |
(ab) | 分组 |
^a | 匹配开头 |
a$ | 匹配结尾 |
. | 任意字符 |
[abc] | 字符类 |
[a-c] | 字符类区间 |
[a-z0-9_] | 字符类组合 |
[^abc] | 取反字符类 |
[^a-c] | 取反字符类区间 |
[\b] | 退格符(U+0008) |
\|、\、... | 转义字符 |
\f | 换页符(U+000C) |
\n | 换行符(U+000A) |
\r | 回车符(U+000D) |
\t | Tab(U+0009) |
\v | 垂直制表符(U+000B) |
如果 Schema 中不使用pattern/patternProperties,可以把两个宏都置 0 彻底关闭该功能以减小代码体积。对应的宏定义位于 schema.h:
RAPIDJSON_SCHEMA_USE_INTERNALREGEX(默认 1):使用内置 NFA 引擎;RAPIDJSON_SCHEMA_USE_STDREGEX:C++11 编译器下可设为 1 改用std::regex(非 C++11 环境会自动置 0);- 两者都为 0 时禁用 pattern 相关功能。
性能
由于多数 C++ JSON 库尚不支持 JSON Schema,官方按 json-schema-benchmark——其SetUp()会加载jsonschema/tests/draft4/下的 28 个测试文件(type.json、allOf.json、refRemote.json等),对每个SchemaDocument循环 100000 轮验证并统计"每秒测试数"。
在 Mac Book Pro(2.8 GHz Intel Core i7)上收集的结果:
| Validator | Relative speed | 每秒测试数 |
|---|---|---|
| RapidJSON | 155% | 30682 |
| ajv | 100% | 19770 (± 1.31%) |
| is-my-json-valid | 70% | 13835 (± 2.84%) |
| jsen | 57.7% | 11411 (± 1.27%) |
| schemasaurus | 26% | 5145 (± 1.62%) |
| themis | 19.9% | 3935 (± 2.69%) |
| z-schema | 7% | 1388 (± 0.84%) |
| jsck | 3.1% | 606 (± 2.84%) |
| jsonschema | 0.9% | 185 (± 1.01%) |
| skeemas | 0.8% | 154 (± 0.79%) |
| tv4 | 0.5% | 93 (± 0.94%) |
| jayschema | 0.1% | 21 (± 1.14%) |
即 RapidJSON 比最快的 JavaScript 库(ajv)快约 1.5 倍,比最慢的快约 1400 倍。以上数据以官方文档公布为准,跨语言对比仅作量级参考。
违规报告(Error Reporting):GetError() 的结构
验证实例时往往不仅需要知道"合法/非法",还需要知道具体违反了什么。SchemaValidator(以及SchemaValidatingReader)会把验证过程中遇到的错误收集进一个 JSONValue,通过validator.GetError()访问;同时SchemaDocument在编译阶段发现 Schema 本身有问题时(如引用了未知 Schema),也通过schema.GetError()暴露。
错误对象的结构没有业界标准,官方声明其在未来版本可能变化。总体约定如下:
- 验证产生一个错误值,始终是对象;空对象
{}表示实例合法; - 每个成员的名字是被违反的 JSON Schema 关键字;
- 成员值是描述单个违规的对象,或此类对象的数组;
- 每个违规对象必含两个字符串成员:
instanceRef:指向实例中检测到违规的子对象的 JSON Pointer 的 URI fragment 序列化;schemaRef:Schema 的 URI 加上指向被违反子 Schema 的 JSON Pointer fragment。
完整示例
对实例{"numbers": [1, 2, "3", 4, 5]},用如下 Schema 验证:
{ "type": "object", "properties": { "numbers": {"$ref": "numbers.schema.json"} } }其中numbers.schema.json(通过IRemoteSchemaDocumentProvider提供)为:
{ "type": "array", "items": {"type": "number"} }产生的错误对象为:
{ "type": { "instanceRef": "#/numbers/2", "schemaRef": "numbers.schema.json#/items", "expected": ["number"], "actual": "string" } }示例程序 schemavalidator.cpp 中的CreateErrorMessages()递归遍历该结构,把oneOf/allOf/anyOf/dependencies的嵌套子错误逐层展开,并借助GetValidateError_En()输出人类可读消息——这正是GetError()推荐的消费方式。
各关键字的错误成员明细
数值类
multipleOf:expected(必填,严格大于 0,Schema 中multipleOf的值)、actual(必填,实例值);maximum:expected(必填,Schema 中的maximum值)、exclusiveMaximum(可选布尔,仅当 Schema 指定"exclusiveMaximum": true时出现)、actual(必填);minimum:expected(必填,Schema 中的minimum值)、exclusiveMinimum(可选布尔,规则同上)、actual(必填)。
字符串类
maxLength/minLength:expected(必填,大于等于 0,Schema 中对应关键字的值)、actual(必填字符串,实例值);pattern:只有actual(必填字符串)。之所以不报告期望的 pattern,是因为SchemaDocument的内部表示不保存原始 pattern 字符串(它被编译成了 NFA/正则对象)。
数组类
additionalItems:当items为数组、additionalItems为false、且实例数组元素多于items数组长度时报告;disallowed(必填整数,无对应 Schema 的第一个元素的下标);maxItems/minItems:expected(必填整数,Schema 中的值)、actual(必填整数,实例数组的元素个数);uniqueItems:duplicates(必填数组,元素为下标整数)。出于性能考虑,RapidJSON 只报告前两个相等的项。
对象类
maxProperties/minProperties:expected(必填整数,Schema 中的值)、actual(必填整数,实例对象的属性个数);required:missing(必填,一个或多个唯一字符串的数组),列出required中声明但实例中缺失的属性名;additionalProperties:当 Schema 指定additionalProperties: false且某属性名既不在properties中又不匹配patternProperties的任何正则时报告;disallowed(必填字符串,冒犯的属性名)。出于性能考虑只报告遇到的第一个此类属性;dependencies:errors(必填对象)。注意 Draft-04 同时支持两种依赖:- schema dependency:控制属性存在时,实例对象必须满足从属子 Schema——违反时
errors中以控制属性名为键,值为对从属 Schema 验证产生的错误对象; - property dependency:控制属性存在时要求其他从属于性也存在——违反时对应值为缺失从属于性名的字符串数组。
- schema dependency:控制属性存在时,实例对象必须满足从属子 Schema——违反时
任意类型
enum:除instanceRef和schemaRef外无附加属性。不列出允许的取值(SchemaDocument不保存原始形式),也不报告违规值本身(可能过于庞大)。如需展示给用户,可自行沿instanceRef/schemaRef查回原始数据;type:expected(必填,一个或多个唯一字符串数组,取值为 Draft-04 定义的七种 JSON 原始类型之一,即 Schema 中type允许的类型列表)、actual(必填字符串,实例的实际原始类型);allOf/anyOf/oneOf:errors(必填,对象数组,长度与对应关键字下的子 Schema 数量一致),每个元素是实例对相应子 Schema 验证产生的错误值。规律:allOf至少有一个错误非空;anyOf全部非空;oneOf要么全部非空、要么多于一个为空;not:除instanceRef和schemaRef外无附加属性。
测试与验证入口
- 单元测试:test/unittest/schematest.cpp(约 3600 行)覆盖了各类关键字的正反用例、错误报告结构与远程 provider 行为,是最直接的"该功能应如何工作"参照;
- 性能测试:test/perftest/schematest.cpp 复现了 json-schema-benchmark 的 draft-4 流程,可在本地对比验证速率;
- 可运行示例:example/schemavalidator/schemavalidator.cpp,用法为
schemavalidator schema.json < input.json,输出包含 schema/document 指针、错误码与完整的GetError()报告。
小结与实践建议
- 编译与验证分离:
SchemaDocument只编译一次,多个请求各自复用SchemaValidator(必要时Reset()),这是官方性能数据的正确打开方式; - 大文件走 SAX:仅验证时优先
Reader::Parse(stream, validator),内存占用与文件大小无关;需要 DOM 时再引入SchemaValidatingReader; - 序列化前校验:用
GenericSchemaValidator<SchemaDocument, Writer<...>>把写出过程也纳入校验闭环; - 远程引用:自行实现
IRemoteSchemaDocumentProvider::GetRemoteDocument(),把 URI 解析成已编译的SchemaDocument*返回;注意id关键字与 URI 组合功能尚不完整(对应唯一的测试失败用例); - 错误消费:用
IsValid()+GetInvalidSchemaPointer()/GetInvalidDocumentPointer()快速定位,用GetError()获取可序列化的结构化报告; - 正则策略:默认内置 NFA 引擎;C++11 环境可切
std::regex;不用 pattern 时两个宏都置 0 减小体积; format不校验:字符串format关键字被忽略,如需此类约束须自行补充。
更多背景可参考仓库内 doc/schema.zh-cn.md、doc/pointer.md 与 doc/faq.md。
【免费下载链接】rapidjsonA fast JSON parser/generator for C++ with both SAX/DOM style API项目地址: https://gitcode.com/GitHub_Trending/ra/rapidjson
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考