相关文章推荐
英俊的凉面  ·  Power BI ...·  4 月前    · 
越狱的凉面  ·  MySQL数据类型转换 - 简书·  1 年前    · 
首发于 Milo的编程
RapidJSON v1.1.0 发布简介

RapidJSON v1.1.0 发布简介

时隔 15.6 个月,终于发布了一个 新版本 v1.1.0

新版本除了包含了这些日子收集到的无数的小改进及 bug fixes,也有一些新功能。本文尝试从使用者的角度,简单介绍一下这些功能和沿由。

(题图 Photo by Ian Schneider

JSON Pointer

也许 RapidJSON 一直最为人垢病的地方,是它奇怪的 API 设计。例如,对 DOM 加添数据要给于 allocator 参数:

#include "rapidjson/document.h"
using namespace rapidjson;
// ...
Document d(kObjectType);
Value a(kArrayType);
for (int i = 1; i <= 4; i++)
    a.PushBack(i, d.GetAllocator());
d.AddMember("a", a, d.GetAllocator());
// { a : [1, 2, 3, 4] }

这是由于 RapidJSON 的 DOM 使用局部的分配器,以避免全局分配器的问题。而为了节省内存,每个 Value 不会存储分配器的指针,所以必须从外部提供。

此设计也导致另一种问题。我们看看一个例子,使用 DOM API 访问以下这个 JSON:

{
    "widget": {
        "window": {
            "title": "Sample Konfabulator Widget"

要访问 title,最直觉想到的应该是这样:

Document d;
d.Parse(json);
std::cout << d["widget"]["window"]["title"].GetString();

如果 widget、window 或 title 不存在呢?以标准库 std::map::operator[] 的做法来说,当找不到键,它会自动加入一个键值对,并返回该值(所以 map::operator[] 必须是 non-const 函数)。然而,RapidJSON 创建值的时候需要 allocator,所以不可能自动加入键值对。因此,RapidJSON 规定以 `operator[]` 访问时,必须确保键存在(找不到时直接断言失败)。若不能确保,应先用 HasMember() 判断,或更好的是使用 FindMember(),因为它可以告之键是否存在的同时,能通过迭代器取得该值。可是,使用 FindMember() 去访问多层对象,代码非常冗长:

Value::ConstMemberIterator itr1 = d.FindMember("widget");
if (itr1 != d.MemberEnd()) {
    const Value& widget = itr1->value;
    if (widget.IsObject()) {
        Value::ConstMemberIterator itr2 = widget.FindMember("window");
        if (itr2 != widget.MemberEnd()) {
            const Value& window = itr2->value;
            if (window.IsObject()) {
                Value::ConstMemberIterator itr3 = window.FindMember("title");
                if (itr3 != window.MemberEnd()) {
                    const Value& title = itr3->value;
                    if (title.IsString()) {
                        std::cout << title.GetString();

这坨代码也许是最快最直接的方式。但一般业务代码写成这样,可读性太低,也容易出错。

大家都可以写一些辅助函数来解决这个问题。而我选择了实现 RFC 6901 ── JSON Pointer。先看看使用后的结果:

#include "rapidjson/pointer.h"
// ...
if (const Value* title = GetValueByPointer(d, "/widget/window/title")) {
    if (title->IsString()) {
        std::cout << title->GetString();

这个版本简单得多吧,"/widget/window/title" 是一个 JSON Pointer 的字符串形式,然后 GetValueByPointer() 在 d 上解引用,如果失败就返回空指针。

在逻辑上是和上面的冗长版本是一模一样的,只是增加了一些解析 JSON Pointer 的运行时间及空间成本。对大多数人来说,应该更会接受这个版本。

有时候,业务逻辑还会是这样的:「如果键不存在,就使用缺省值。」RapidJSON 的 JSON Pointer 也提供此功能:

Value& title = GetValueByPointerWithDefault(
    d, "/widget/window/title", "untitled");

当解引用失败时,它会创建整个路径,并把预设值复制成新值,并返回该值。由于它总能返回一个值,此函数的返回类型为引用而不是指针。

在此也简单介绍一下 JSON Pointer 的语法。它以 / 分隔多个 token,而每个 token 可以是 JSON object 的键,也可以是 JSON array 的下标。还有一种特殊 token 是负号 -,它可以指 JSON array 最后元素的下一个元素。使用这种特性能实现 PushBack() 的效果:

Document d;
CreateValueByPointer(d, "/a").SetArray();
for (int i = 1; i <= 4; i++)
    SetValueByPointer(d, "/a/-", i);
// { a : [1, 2, 3, 4] }

使用 JSON Pointer 的另一优点在于,它本身也是一个字符串,可以放置在 JSON 或其他文本格式之中。那么,我们便有一个标准方式去引用 JSON 中的值。

希望 JSON Pointer 能减轻使用者的负担,同时也提供一种数据驱动的弹性。新功能 JSON Pointer 简单介绍至此,更多信息可参考 RapidJSON 使用手册:Pointer

JSON Schema

上面我们也谈到一个问题,JSON 里的组织方式、类型可能和预期的不同,我们可能要写很多代码去校验一个 JSON 的格式是否乎合预期。特别是后台服务器可能接收到不正常的JSON,甚至是恶意编写的 JSON 以图攻击。

在 XML 的世界中,可使用 XML DTD 或 XML Schema 去描述 XML 的结构。在 JSON 的世界中,已经有相关草案,称为 JSON Schema

RapidJSON 实现了 JSON Schema v4 draft,并正式纳入了 v1.1.0。先看看用法:

#include "rapidjson/schema.h"
// ...
Document sd;
if (!sd.Parse(schemaJson).HasParseError()) {
    // 此 schema 不是合法的 JSON
SchemaDocument schema(sd); // 把一个 Document 编译至 SchemaDocument
// 之后不再需要 sd
Document d;
if (!d.Parse(inputJson).HasParseError()) {
    // 输入不是一个合法的 JSON
SchemaValidator validator(schema);
if (!d.Accept(validator)) {
    // 输入的 JSON 不合乎 schema

以我所知,现时所有 JSON Schema 实现都是校验一个 DOM 是否合乎 Schema。RapidJSON 做了一个创新的尝试,以事件流(SAX 风格)的方式去做校验。上面的例子利用 Document::Accept() 产生事件流,然后送交 SchemaValidator 校验。也许读者会问:「这也是在校验一个 DOM 是否合乎 Schema,有什么特别吗?」

这实际意味着,RapidJSON 的 JSON Schema 校验器除了可以校验 DOM,也可以校验更底层的 SAX。例如,我们可以用 SAX 解析 JSON 时,同时进行 JSON Schema 校验。如果中途不合乎 JSON Schema,就能直接中止解析。

SchemaValidator validator(schema);
Reader reader;
if (!reader.Parse(stream, validator)) {
    if (!validator.IsValid()) {
        // 输入的 JSON 不合乎 schema

也可以同时把事件转发至一个自定义 handler:

MyHandler handler;
GenericSchemaValidator<SchemaDocument, MyHandler> validator(schema, handler);
Reader reader;
if (!reader.Parse(stream, validator)) {
    if (!validator.IsValid()) {
        // 输入的 JSON 不合乎 schema

由于 DOM 解析 JSON 时,底层也是使用 SAX,所以也可以同时做 Schema 校验。其实除了解析,在生成时也可以进行校验,以确保输出的 JSON 也是乎合 Schema 的。这些用法都可参考 RapidJSON 使用手册:Schema 。要学习 JSON Schema 的写法,笔者推荐 Understanding JSON Schema 这个英文网站。

C++11 范围 for 循环

此版本还加入了 Array 和 Object 辅助类型(包裹类),可分别通过 Value::GetArray()、Value::GetObject() 获取。这两个辅助类型提供该 JSON 类型专门的接口,例如 Array::PushBack()、Object::AddMember() 等。更重要的是,为了令 C++11 用户使用得更顺手,它们可做范围 for 循环(range-based for loop):

// C++03
for (Value::ConstValueIterator itr = a.Begin(); itr != a.End(); ++itr)
    printf("%d ", itr->GetInt());
// C++11
for (auto& v : a.GetArray())
    printf("%d ", v.GetInt());
// C++03
for (Value::ConstMemberIterator itr = document.MemberBegin();
    itr != document.MemberEnd(); ++itr)
    printf("Type of member %s is %s\n",
        itr->name.GetString(), kTypeNames[itr->value.GetType()]);