
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 行涉及三个关键类型类型源码位置职责SchemaDocumentschema.h编译后的 Schema可被多个校验器共享校验过程中不被修改SchemaValidatorschema.h#L3178SAX Handler接收 SAX 事件并即时判定合法性SchemaValidatingReaderschema.h#L3199组合 Reader Validator Document 的辅助类从源码看SchemaValidator实际是GenericSchemaValidatorSchemaDocument的 typedeftypedef GenericSchemaValidatorSchemaDocument 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; WriterStringBuffer 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 类型GenericPointerStringifyUriFragment()将其序列化为#/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; SchemaValidatingReaderkParseDefaultFlags, 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则要用模板类显式指定输出 handlerMyHandler handler; GenericSchemaValidatorSchemaDocument, MyHandler validator(schema, handler); Reader reader; if (!reader.Parse(ss, validator)) { if (!validator.IsValid()) { // ... } }序列化时的校验也可以反过来在写出 JSON 的过程中做验证确保序列化结果符合 SchemaStringBuffer sb; WriterStringBuffer writer(sb); GenericSchemaValidatorSchemaDocument, WriterStringBuffer 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。远程 SchemaIRemoteSchemaDocumentProvider 与 $refJSON 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字段。规范符合性ConformanceRapidJSON 在 JSON Schema Test SuiteDraft-4 部分中通过了263 个测试中的 262 个。唯一的失败用例是refRemote.json中 change resolution scope 的 changed scope ref invalid原因是id关键字与 URI 组合功能尚未实现。另外两点注意字符串的format关键字被忽略因为规范并未要求实现它pattern与patternProperties依赖正则表达式默认使用 RapidJSON 自研的 NFA 正则引擎include/rapidjson/internal/regex.h。内置正则引擎支持的语法SyntaxDescriptionab串联Concatenationa\|b选择Alternationa?0 次或 1 次a*0 次或多次a1 次或多次a{3}恰好 3 次a{3,}至少 3 次a{3,5}3 到 5 次(ab)分组^a匹配开头a$匹配结尾.任意字符[abc]字符类[a-c]字符类区间[a-z0-9_]字符类组合[^abc]取反字符类[^a-c]取反字符类区间[\b]退格符U0008\|、\、...转义字符\f换页符U000C\n换行符U000A\r回车符U000D\tTabU0009\v垂直制表符U000B如果 Schema 中不使用pattern/patternProperties可以把两个宏都置 0 彻底关闭该功能以减小代码体积。对应的宏定义位于 schema.hRAPIDJSON_SCHEMA_USE_INTERNALREGEX默认 1使用内置 NFA 引擎RAPIDJSON_SCHEMA_USE_STDREGEXC11 编译器下可设为 1 改用std::regex非 C11 环境会自动置 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 Pro2.8 GHz Intel Core i7上收集的结果ValidatorRelative speed每秒测试数RapidJSON155%30682ajv100%19770 (± 1.31%)is-my-json-valid70%13835 (± 2.84%)jsen57.7%11411 (± 1.27%)schemasaurus26%5145 (± 1.62%)themis19.9%3935 (± 2.69%)z-schema7%1388 (± 0.84%)jsck3.1%606 (± 2.84%)jsonschema0.9%185 (± 1.01%)skeemas0.8%154 (± 0.79%)tv40.5%93 (± 0.94%)jayschema0.1%21 (± 1.14%)即 RapidJSON 比最快的 JavaScript 库ajv快约 1.5 倍比最慢的快约 1400 倍。以上数据以官方文档公布为准跨语言对比仅作量级参考。违规报告Error ReportingGetError() 的结构验证实例时往往不仅需要知道合法/非法还需要知道具体违反了什么。SchemaValidator以及SchemaValidatingReader会把验证过程中遇到的错误收集进一个 JSONValue通过validator.GetError()访问同时SchemaDocument在编译阶段发现 Schema 本身有问题时如引用了未知 Schema也通过schema.GetError()暴露。错误对象的结构没有业界标准官方声明其在未来版本可能变化。总体约定如下验证产生一个错误值始终是对象空对象{}表示实例合法每个成员的名字是被违反的 JSON Schema 关键字成员值是描述单个违规的对象或此类对象的数组每个违规对象必含两个字符串成员instanceRef指向实例中检测到违规的子对象的 JSON Pointer 的 URI fragment 序列化schemaRefSchema 的 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()推荐的消费方式。各关键字的错误成员明细数值类multipleOfexpected必填严格大于 0Schema 中multipleOf的值、actual必填实例值maximumexpected必填Schema 中的maximum值、exclusiveMaximum可选布尔仅当 Schema 指定exclusiveMaximum: true时出现、actual必填minimumexpected必填Schema 中的minimum值、exclusiveMinimum可选布尔规则同上、actual必填。字符串类maxLength/minLengthexpected必填大于等于 0Schema 中对应关键字的值、actual必填字符串实例值pattern只有actual必填字符串。之所以不报告期望的 pattern是因为SchemaDocument的内部表示不保存原始 pattern 字符串它被编译成了 NFA/正则对象。数组类additionalItems当items为数组、additionalItems为false、且实例数组元素多于items数组长度时报告disallowed必填整数无对应 Schema 的第一个元素的下标maxItems/minItemsexpected必填整数Schema 中的值、actual必填整数实例数组的元素个数uniqueItemsduplicates必填数组元素为下标整数。出于性能考虑RapidJSON 只报告前两个相等的项。对象类maxProperties/minPropertiesexpected必填整数Schema 中的值、actual必填整数实例对象的属性个数requiredmissing必填一个或多个唯一字符串的数组列出required中声明但实例中缺失的属性名additionalProperties当 Schema 指定additionalProperties: false且某属性名既不在properties中又不匹配patternProperties的任何正则时报告disallowed必填字符串冒犯的属性名。出于性能考虑只报告遇到的第一个此类属性dependencieserrors必填对象。注意 Draft-04 同时支持两种依赖schema dependency控制属性存在时实例对象必须满足从属子 Schema——违反时errors中以控制属性名为键值为对从属 Schema 验证产生的错误对象property dependency控制属性存在时要求其他从属于性也存在——违反时对应值为缺失从属于性名的字符串数组。任意类型enum除instanceRef和schemaRef外无附加属性。不列出允许的取值SchemaDocument不保存原始形式也不报告违规值本身可能过于庞大。如需展示给用户可自行沿instanceRef/schemaRef查回原始数据typeexpected必填一个或多个唯一字符串数组取值为 Draft-04 定义的七种 JSON 原始类型之一即 Schema 中type允许的类型列表、actual必填字符串实例的实际原始类型allOf/anyOf/oneOferrors必填对象数组长度与对应关键字下的子 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序列化前校验用GenericSchemaValidatorSchemaDocument, Writer...把写出过程也纳入校验闭环远程引用自行实现IRemoteSchemaDocumentProvider::GetRemoteDocument()把 URI 解析成已编译的SchemaDocument*返回注意id关键字与 URI 组合功能尚不完整对应唯一的测试失败用例错误消费用IsValid()GetInvalidSchemaPointer()/GetInvalidDocumentPointer()快速定位用GetError()获取可序列化的结构化报告正则策略默认内置 NFA 引擎C11 环境可切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),仅供参考