Работа с JSON на C++ при помощи библиотеки RapidJSON

28 августа 2017

JSON используется очень много где. В частности, он нужен для написания REST-сервисов и REST-клиентов. Если какие-то объекты нужно сериализовывать и класть в какой-нибудь Redis, использовать в качестве сериализованного представления JSON также будет неплохой идеей. Наконец, информация в базе данных часто хранится в виде JSON просто из соображений удобства изменения схемы. Так давайте же выясним, как работать с JSON, если вы пишите на C++.

Соответствующих библиотек существует немало. Я выбрал RapidJSON, поскольку опыт работы с ней имел мой знакомый гуру C++, господин @yowidin. Он же помог мне разобраться с одной проблемой, возникшей при использовании этой библиотеки.

Помимо прочего, библиотека интересна тем, что является header-only и self-contained. Первое свойство довольно удобно, в частности, потому что библиотека не завязана на какую-то систему сборки. Вы просто копируете код библиотеки или тяните ее при помощи сабмодулей git, инклудите в своем коде заголовочные файлы, и все работает. Второе свойство означает, что у библиотеки нет каких-либо зависимостей. В действительности, она не зависит даже от STL.

В качестве примера рассмотрим сериализацию и десериализацю следующих объектов:

class Date {
public:
    Date(uint16_t year, uint8_t month, uint8_t day)
      : _year(year)
      , _month(month)
      , _day(day) {
    }

    /* getYear/setYear, getMonth/setMonth etc skipped */

private:
    uint16_t _year;
    uint8_t _month;
    uint8_t _day;
};

class User {
public:
    User(uint64_t id, const std::string& name, uint64_t phone,
         Date birthday)
      : _id(id)
      , _name(name)
      , _phone(phone)
      , _birthday(birthday) {
    }

    /* getId/setId, getName/setName etc skipped */

private:
    uint64_t _id;
    std::string _name;
    uint64_t _phone;
    Date _birthday;
};

Сериализация и десериализация класса Date пишется довольно просто:

class Date {
public:

/* ... */

    rapidjson::Document toJSON() {
        rapidjson::Document doc;
        auto& allocator = doc.GetAllocator();
        doc.SetArray()
           .PushBack(_year, allocator)
           .PushBack(_month, allocator)
           .PushBack(_day, allocator);
        return doc;
    }

    static Date fromJSON(const rapidjson::Value& doc) {
        if(!doc.IsArray())
            throw std::runtime_error("document is not an array");

        if(doc.Size() != 3)
            throw std::runtime_error("wrong array size");

        uint16_t year = doc[0].GetInt();
        uint8_t month = doc[1].GetInt();
        uint8_t day = doc[2].GetInt();

        Date result(year, month, day);
        return result;
    }

/* ... */

};

Как видите, в качестве JSON-представления класса Date был выбран массив из трех чисел. Считаю, что приведенный код достаточно простой. Поэтому сразу перейдем к сериализации и десериализации класса User:

class User {
public:

/* ... */

    rapidjson::Document toJSON() {
        rapidjson::Value json_val;
        rapidjson::Document doc;
        auto& allocator = doc.GetAllocator();

        doc.SetObject();

        json_val.SetUint64(_id);
        doc.AddMember("id", json_val, allocator);

        json_val.SetString(_name.c_str(), allocator);
        doc.AddMember("name", json_val, allocator);

        // see http://rapidjson.org/md_doc_tutorial.html#DeepCopyValue
        json_val.CopyFrom(_birthday.toJSON(), allocator);
        doc.AddMember("birthday", json_val, allocator);

        json_val.SetUint64(_phone);
        doc.AddMember("phone", json_val, allocator);

        return doc;
    }

    static User fromJSON(const rapidjson::Value& doc) {
        if(!doc.IsObject())
            throw std::runtime_error("document should be an object");

        static const char* members[] = { "id", "name", "phone",
                                         "birthday" };
        for(size_t i = 0; i < sizeof(members)/sizeof(members[0]); i++)
            if(!doc.HasMember(members[i]))
                throw std::runtime_error("missing fields");

        uint64_t id = doc["id"].GetUint64();
        std::string name = doc["name"].GetString();
        uint64_t phone = doc["phone"].GetUint64();
        Date birthday = Date::fromJSON(doc["birthday"]);

        User result(id, name, phone, birthday);
        return result;
    }

/* ... */

};

Как видите, приведенный код тоже не слишком сложен. Единственный тонкий момент заключается в том, что в методе User.toJSON() мы должные сделать глубокую копию сериализованного представления класса Date. Дело в том, что из соображений производительности в RapidJSON по умолчанию используется не глубокое копирование. В данном случае это привело бы к тому, что копия объекта rapidjson::Document, возвращаемая из метода, ссылалась бы на данные из фрейма стека метода.

Также стоит отметить, что все проверки в приведенном коде вроде .IsArray(), .IsObject() и .HasMember() нужны исключительно для демонстрации наличия таких методов, а также получения чуть более читаемых исключений. Вы не много потеряете, и даже немного выиграете по производительности, если не станете загромождать ими код вашего приложения, так как RapidJSON все равно сделает все требуемые проверки.

Теперь рассмотрим преобразование rapidjson::Document в строку:

rapidjson::Document doc = user.toJSON();
rapidjson::StringBuffer buffer;
// rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
rapidjson::PrettyWriter<rapidjson::StringBuffer> writer(buffer);
doc.Accept(writer);

const std::string& str = buffer.GetString();
std::cout << "Serialized:" << std::endl;
std::cout << str << std::endl;

Можно использовать класс Writer или PrettyWriter, в зависимости от того, хотите ли вы получить компактную или красивую строку.

И, наконец, пример обратного преобразования:

rapidjson::Document doc2;
doc2.Parse(str.c_str());
User decodedUser = User::fromJSON(doc2);

Как видите, RapidJSON — довольно приятная в использовании библиотека. Полную версию исходного кода к этой заметке вы найдете на GitHub. Увы, в рамках одного поста нельзя рассмотреть абсолютно все возможности RapidJSON. В частности, библиотека поддерживает JSON Schema, а также SAX, то есть, потоковые генерацию и парсинг JSON. Подробности вы найдете в официальной документации, которая у RapidJSON весьма хороша.

А приходилось ли вам работать с JSON на C++, и если да, то какие библиотеки вы для этого использовали и каковы ваши впечатления от них?

Дополнение: Сериализация и десериализация в/из Protobuf на C++

Метки: .


Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.