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