← На главную

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

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++