Работа с JSON на C++ при помощи библиотеки RapidJSON
28 августа 2017
JSON используется очень много где. В частности, он нужен для написания REST-сервисов и REST-клиентов. Если какие-то объекты нужно сериализовывать и класть в какой-нибудь Redis, использовать в качестве сериализованного представления JSON также будет неплохой идеей. Наконец, информация в базе данных часто хранится в виде JSON просто из соображений удобства изменения схемы. Так давайте же выясним, как работать с JSON, если вы пишите на C++.
Соответствующих библиотек существует немало. Я выбрал RapidJSON, поскольку опыт работы с ней имел мой знакомый гуру C++, господин @yowidin. Он же помог мне разобраться с одной проблемой, возникшей при использовании этой библиотеки.
Помимо прочего, библиотека интересна тем, что является header-only и self-contained. Первое свойство довольно удобно, в частности, потому что библиотека не завязана на какую-то систему сборки. Вы просто копируете код библиотеки или тяните ее при помощи сабмодулей git, инклудите в своем коде заголовочные файлы, и все работает. Второе свойство означает, что у библиотеки нет каких-либо зависимостей. В действительности, она не зависит даже от STL.
В качестве примера рассмотрим сериализацию и десериализацю следующих объектов:
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 пишется довольно просто:
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:
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::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
, в зависимости от того, хотите ли вы получить компактную или красивую строку.
И, наконец, пример обратного преобразования:
doc2.Parse(str.c_str());
User decodedUser = User::fromJSON(doc2);
Как видите, RapidJSON — довольно приятная в использовании библиотека. Полную версию исходного кода к этой заметке вы найдете на GitHub. Увы, в рамках одного поста нельзя рассмотреть абсолютно все возможности RapidJSON. В частности, библиотека поддерживает JSON Schema, а также SAX, то есть, потоковые генерацию и парсинг JSON. Подробности вы найдете в официальной документации, которая у RapidJSON весьма хороша.
А приходилось ли вам работать с JSON на C++, и если да, то какие библиотеки вы для этого использовали и каковы ваши впечатления от них?
Дополнение: Сериализация и десериализация в/из Protobuf на C++
Метки: C/C++.
Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.