rapidjson/doc/schema.zh-cn.md
2016-02-03 11:33:27 +08:00

9.4 KiB
Raw Blame History

Schema

状态: 实验性,应该会合进 v1.1

JSON Schema 是描述 JSON 格式的一个标准草案。一个 schema 本身也是一个 JSON。使用 JSON Schema 去校验 JSON可以让你的代码安全地访问 DOM而无须检查类型或键值是否存在等。这也能确保输出的 JSON 是符合指定的 schema。

RapidJSON 实现了一个 JSON Schema Draft v4 的校验器。若你不熟悉 JSON Schema可以参考 Understanding JSON Schema

[TOC]

基本用法

首先,你要把 JSON Schema 解析成 Document,再把它编译成一个 SchemaDocument

然后,利用该 SchemaDocument 创建一个 SchemaValidator。它与 Writer 相似,都是能够处理 SAX 事件的。因此,你可以用 document.Accept(validator) 去校验一个 JSON然后再获取校验结果。

#include "rapidjson/schema.h"

// ...

Document sd;
if (!sd.Parse(schemaJson)) {
    // the schema is not a valid JSON.
    // ...       
}
SchemaDocument schema(sd); // Compile a Document to SchemaDocument
// sd is no longer needed here.

Document d;
if (!d.Parse(inputJson)) {
    // 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());
}

一些注意点:

  • 一个 SchemaDocment 能被多个 SchemaValidator 吊用。它不会被 SchemaValidator 修改。
  • 一个 SchemaValidator 可以重复使用来校验多个文件。在校验其他文件前,先调用 validator.Reset()

在解析/生成时进行校验

与大部分 JSON Schema 校验器有所不同RapidJSON 提供了一个基于 SAX 的 schema 校验器实现。因此,你可以在输入流解析 JSON 的同时进行校验。若校验器遇到一个与 schema 不符的值,就会立即终止解析。这设计对于解析大型 JSON 文件时特别有用。

DOM 解析

在使用 DOM 进行解析时,Document 除了接收 SAX 事件外,还需做一些准备及结束工作,因此,为了连接 ReaderSchemaValidatorDocument 要做多一点事情。SchemaValidatingReader 是一个辅助类去做那些工作。

#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());
    }
}

SAX 解析

使用 SAX 解析时,情况就简单得多。若只需要校验 JSON 而无需进一步处理,那么仅需要:

SchemaValidator validator(schema);
Reader reader;
if (!reader.Parse(stream, validator)) {
    if (!validator.IsValid()) {
        // ...    
    }
}

这种方式和 schemavalidator 例子完全相同。这带来的独特优势是,无论 JSON 多巨大,永远维持低内存用量(内存用量只与 Schema 的复杂度相关)。

若你需要进一步处理 SAX 事件,便可使用模板类 GenericSchemaValidator 去设置校验器的输出 Handler

MyHandler handler;
GenericSchemaValidator<SchemaDocument, MyHandler> validator(schema, handler);
Reader reader;
if (!reader.Parse(ss, validator)) {
    if (!validator.IsValid()) {
        // ...    
    }
}

生成

我们也可以在生成serialization的时候进行校验。这能确保输出的 JSON 符合一个 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()) {
        // ...
    }
}

当然,如果你的应用仅需要 SAX 风格的生成,那么只需要把 SAX 事件由原来发送到 Writer,改为发送到 SchemaValidator

远程 Schema

JSON Schema 支持 $ref 关键字,它是一个JSON pointer 引用至一个本地local或远程remote schema。本地指针的首字符是 #,而远程指针是一个相对或绝对 URI。例如

{ "$ref": "definitions.json#/address" }

由于 SchemaValidator 并不知道如何处理那些 URI它需要使用者提供一个 IRemoteSchemaDocumentProvider 的实例去处理。

class MyRemoteSchemaDocumentProvider : public IRemoteSchemaDocumentProvider {
public:
    virtual const SchemaDocument* GetRemoteDocument(const char* uri, SizeTyp length) {
        // Resolve the uri and returns a pointer to that schema.
    }
};

// ...

MyRemoteSchemaDocumentProvider provider;
SchemaValidator validator(schema, &provider);

标准的符合程度

RapidJSON 通过了 JSON Schema Test Suite (Json Schema draft 4) 中 263 个测试的 262 个。

没通过的测试是 refRemote.json 中的 "change resolution scope" - "changed scope ref invalid"。这是由于未实现 id schema 关键字及 URI 合并功能。

除此以外,关于字符串类型的 format schema 关键字也会被忽略,因为标准中并没需求必须实现。

正则表达式

patternpatternProperties 这两个 schema 关键字使用了正则表达式去匹配所需的模式。

RapidJSON 实现了一个简单的 NFA 正则表达式引擎,并预设使用。它支持以下语法。

语法 描述
ab 串联
`a b`
a? 零或一次
a* 零或多次
a+ 一或多次
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 制表 (U+0009)
\v 垂直制表 (U+000B)

对于使用 C++11 编译器的使用者,也可使用 std::regex,只需定义 RAPIDJSON_SCHEMA_USE_INTERNALREGEX=0RAPIDJSON_SCHEMA_USE_STDREGEX=1。若你的 schema 无需使用 patternpatternProperties,可以把两个宏都设为零,以禁用此功能,这样做可节省一些代码体积。

性能

大部分 C++ JSON 库都未支持 JSON Schema。因此我们尝试按照 json-schema-benchmark 去评估 RapidJSON 的 JSON Schema 校验器。该评测测试了 11 个运行在 node.js 上的 JavaScript 库。

该评测校验 JSON Schema Test Suite 中的测试,当中排除了一些测试套件及个别测试。我们在 schematest.cpp 实现了相同的评测。

在 MacBook Pro (2.8 GHz Intel Core i7) 上收集到以下结果。

校验器 相对速度 每秒执行的测试数目
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.5x。比最慢的快 1400x。