среда, 23 марта 2022 г.

[prog.c++] Набиваю шишки с nlohmann::json и uniform initialization syntax

В проекте, который наша маленькая компания сейчас делает под заказ, до недавнего времени использовалась связка из RapidJSON и json_dto. Использовалась буквально в паре-тройке мест и в минимальном объеме.

Но вот пришла необходимость задействовать в проекте JSON более плотно, как основное средство для обмена разнообразной информацией между сущностями в программе. Для чего в проекте RapidJSON и json_dto были заменены на nlohmann::json. Выбран был nlohmann::json поскольку потребовался именно что удобный C++ный DSL для формирования JSON-значений.

До этого у меня опыта работы с nlohmann::json не было, поэтому не удивительно, что умудрился наступить на грабли, обусловленные как самим nlohmann::json, так и C++ным uniform initialization syntax, так и моим дилетантизмом в этой области. О чем сегодня и попробую рассказать.

Итак, берем текущую версию nlohmann::json (на данный момент это 3.10.5) и делаем вот такой вот элементарный пример:

#include <iostream>

#include "json.hpp"

struct test_data
{
   std::string a;
};

void to_json( nlohmann::json & json, const test_data & o )
{
   json = nlohmann::json{ "a", o.a };
}

int main()
{
   nlohmann::json a{ test_data{ "1" } };
   std::cout << a << std::endl;
}

Компилируем это все 11-ым GCC с ключиком `-std=c++17`, запускаем и получаем вполне ожидаемый результат:

{"a":"1"}

Отлично.

Теперь расширяем тип test_data еще одним полем:

#include <iostream>

#include "json.hpp"

struct test_data
{
   std::string a;
   std::string b;
};

void to_json( nlohmann::json & json, const test_data & o )
{
   json = nlohmann::json{ {"a", o.a}, {"b", o.b} };
}

int main()
{
   nlohmann::json a{ test_data{ "1""2" } };
   std::cout << a << std::endl;
}

Компилируем, запускаем и видим:

[{"a":"1","b":"2"}]

А это уже не то, что нужно. У нас какие-то квадратные скобочки обозначились. Что это?

У нас получился JSON-объект, который является массивом (array в нотации JSON) с единственным элементом, который уже является объектом (object в нотации JSON).

Тогда как хотелось иметь просто JSON-объект который именно что object, а не array с единственным object-ом внутри.

Почему так происходит?

Насколько я понял, когда у нас используется uniform initialization syntax для создания JSON-а из не-JSON-объекта (т.е. нужно автоматически сконвертировать test_data в JSON), то задействуются два конструктора для nlohmann::basic_json.

Сперва вот этот, который и трансформирует test_data в экземпляр basic_json. И это именно такой экземпляр, который мне и нужен (т.е. это именно что object).

Затем этот экземпляр преобразуется в содержимое initializer_list_t, а полученный initializer_list_t уже отдается в другой конструктор, вот в этот. Этот конструктор пытается понять является ли переданный ему initializer_list набором из пар (строковое имя, значение). Если да, то из initializer_list строится object, если нет, то array.

Так вот, когда test_data преобразуется первым конструктором в basic_json, то в initializer_list попадает всего один элемент и этот элемент явно не похож на пару из (строкового имени, значения). Поэтому конструктор создает array, элементом которого уже будет единственный object.

Отсюда я и получаю array там, где хочу видеть лишь object.

Избавиться от этого просто, достаточно делать вызов конструктора для basic_json без использования фигурных скобочек:

int main()
{
   nlohmann::json a( test_data{ "1""2" } );
   std::cout << a << std::endl;
}

Тогда-то и получается нужный мне результат:

{"a":"1","b":"2"}

Но, если кому-то кажется, что приключения на этом закончились, то это еще не все ;)

Давайте чуть модифицируем исходный пример и используем там круглые скобки вместо квадратных при вызове конструктора basic_json:

#include <iostream>

#include "json.hpp"

struct test_data
{
   std::string a;
};

void to_json( nlohmann::json & json, const test_data & o )
{
   json = nlohmann::json{ "a", o.a };
}

int main()
{
   nlohmann::json a( test_data{ "1" } );
   std::cout << a << std::endl;
}

Что мы получим?

Упс:

["a","1"]

Массив мы получили.

Тэкс... Как бы это исправить? Может быть вот так:

int main()
{
   auto a = nlohmann::json::object( {test_data{ "1" }} );
   std::cout << a << std::endl;
}

Да, так получаем то, что нужно:

{"a":"1"}

Но что будет, если таким же макаром мы попробуем преобразовать в JSON структуру с двумя полями?

#include <iostream>

#include "json.hpp"

struct test_data
{
   std::string a;
   std::string b;
};

void to_json( nlohmann::json & json, const test_data & o )
{
   json = nlohmann::json{ {"a", o.a}, {"b", o.b} };
}

int main()
{
   auto a = nlohmann::json::object( {test_data{ "1""2" }} );
   std::cout << a << std::endl;
}

А будет исключение:

terminate called after throwing an instance of 'nlohmann::detail::type_error'
  what():  [json.exception.type_error.301] cannot create object from initializer list

На этом мое понимание того, как же правильно и предсказуемо использовать nlohmann::json поломалось :)


В общем, uniform initialization syntax в очередной раз преподнес неприятный сюрприз. А жаль, вроде как я уже привык к его использованию и давно уже не набивал шишек из-за uniform initialization syntax в коде.

PS. Конечно, у меня ну совсем мало опыта с nlohmann::json, но все-таки пока нахожусь при мнении, что для простой сериализации/десериализации C++ных структур в/из JSON в режиме DOM наш json_dto удобнее, чем nlohmann::json.

Комментариев нет: