В проекте, который наша маленькая компания сейчас делает под заказ, до недавнего времени использовалась связка из 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.
Комментариев нет:
Отправить комментарий