Как использовать Cereal для десериализации массива Json без окружающего объекта с парами ключ/значение.

Вопрос или проблема

Предположим, у нас есть API, который возвращает ответ в формате json в виде

[1,2,3]

Согласно спецификации json, это допустимый Json. Если я попытаюсь десериализовать его в std::vector<int> с помощью:

#include <cassert>
#include <iostream>
#include <sstream>
#include <cereal/archives/json.hpp>
#include <cereal/types/vector.hpp>

int main() {
    std::stringstream ss("[1,2,3]");
    cereal::JSONInputArchive ar(ss);
    std::vector<int> vec;
    ar(vec);  // src/main.cpp:42  <-- здесь
    assert(vec.size() == 3);
    std::cout << vec[0] << ", " << vec[1] << ", " << vec[2] << std::endl;
    return 0;
}

Я получаю исключение:

terminate called after throwing an instance of 'cereal::RapidJSONException'
  what():  rapidjson internal assertion failure: IsObject()
Aborted (core dumped)

с трассировкой стека, указывающей на:

#0  0x00007ffff7e294a1 in __cxa_throw () from /lib/x86_64-linux-gnu/libstdc++.so.6
#1  0x000055555555a703 in rapidjson::GenericValue<rapidjson::UTF8<char>, rapidjson::MemoryPoolAllocator<rapidjson::CrtAllocator> >::MemberBegin (this=<optimized out>)
    at /usr/local/include/cereal/external/rapidjson/document.h:1168
#2  cereal::JSONInputArchive::startNode (this=0x7fffffffdc80)
    at /usr/local/include/cereal/archives/json.hpp:610
#3  0x00005555555567c8 in cereal::prologue<std::vector<int, std::allocator<int> >, (cereal::traits::detail::sfinae)0> (ar=...) at /usr/local/include/cereal/archives/json.hpp:851
#4  cereal::InputArchive<cereal::JSONInputArchive, 0u>::process<std::vector<int, std::allocator<int> >&> (head=std::vector длиной 0, емкость 0, this=0x7fffffffdc80)
    at /usr/local/include/cereal/cereal.hpp:853
#5  cereal::InputArchive<cereal::JSONInputArchive, 0u>::operator()<std::vector<int, std::allocator<int> >&> (this=0x7fffffffdc80) at /usr/local/include/cereal/cereal.hpp:730
#6  main () at /home/pptaszni/workspace/FooBar/src/main.cpp:42

Обратите внимание, что аналогичный код, где json имеет пару ключ/значение, работает нормально:

int main() {
    std::stringstream ss{"{\"value0\":[1,2,3]}"};
    cereal::JSONInputArchive ar(ss);
    std::vector<int> vec;
    ar(cereal::make_nvp( "value0", vec));
    std::cout << vec.size() << std::endl;
    std::cout << vec[0] << ", " << vec[1] << ", " << vec[2] << std::endl;
    return 0;
}

Как десериализовать строку Json в виде [1,2,3] с помощью Cereal?

Конструкция

cereal::JSONInputArchive ar(ss);

оказалась успешной, что означает, что основной rapidjson::Document не имеет проблем с таким json. Это можно подтвердить следующим кодом:

int main() {
    std::stringstream ss{"[1,2,3]"};
    rapidjson::IStreamWrapper isw(ss);
    rapidjson::Document doc;
    doc.ParseStream<>(isw);
    assert(doc.IsArray());
    for (size_t i = 0; i < doc.Size(); i++) {
        std::cout << doc[i].GetInt() << std::endl;
    }
}

Трассировка стека указывает на метод class InputArchive:

      //! Сериализует данные после вызова пролога, затем вызывает эпилог
      template <class T> inline
      void process( T && head )
      {
        prologue( *self, head );
        self->processImpl( head );
        epilogue( *self, head );
      }

который выбирает перегрузку prologue( JSONOutputArchive & ar, T const & ) для “всех других типов для JSON архивов”, что, по-видимому, включает в себя std::vector<int>. Затем мы попадаем в проблемный код:

if(itsIteratorStack.back().value().IsArray())
  itsIteratorStack.emplace_back(itsIteratorStack.back().value().Begin(), itsIteratorStack.back().value().End());
else
  itsIteratorStack.emplace_back(itsIteratorStack.back().value().MemberBegin(), itsIteratorStack.back().value().MemberEnd());

который выбрасывает исключение, потому что наш itsIteratorStack.back().value() является ни Array, ни Object. Это Number.

Возможным обходным решением этой проблемы является предоставление пустых реализаций функций prologue и epilogue, которые являются лучшими кандидатами для перегрузки, чем шаблоны в заголовке archives/json.hpp:

namespace cereal {
void prologue(cereal::JSONInputArchive&, const std::vector<int>&){}
void epilogue(cereal::JSONInputArchive&, const std::vector<int>&){}
}

int main() {
    std::stringstream ss("[1,2,3]");
    cereal::JSONInputArchive ar(ss);
    std::vector<int> vec;
    ar(vec);
    assert(vec.size() == 3);
    std::cout << vec[0] << ", " << vec[1] << ", " << vec[2] << std::endl;
    return 0;
}

Обмен знаниями вдохновлён этим закрытым вопросом и ответом на проблему с OutputArchive.

Ответ или решение

Для десериализации JSON-массива, представленного в следующем виде: [1,2,3], с использованием библиотеки Cereal, возникает проблема, так как библиотека ожидает, что данные будут обернуты в объект с парой ключ/значение. В данной статье мы подробно разберем, как обойти это ограничение и успешно десериализовать массив в std::vector<int>.

Проблема

При попытке десериализации массива std::vector<int> с помощью класса cereal::JSONInputArchive, возникает исключение cereal::RapidJSONException, связанное с тем, что библиотека RapidJSON пытается обработать входные данные как объект, тогда как в действительности это массив. В случае, если вы предоставите JSON-строку в формате { "value": [1, 2, 3] }, десериализация проходит успешно, так как RapidJSON ожидает объект и корректно обрабатывает его.

Решение

Одним из способов обойти это ограничение является переопределение поведения сериализации и десериализации для std::vector<int>, создавая пустые реализации функций prologue и epilogue. Это позволит Cereal корректно обрабатывать JSON-массив напрямую.

Пошаговая реализация:

  1. Определение пустых функций prologue и epilogue: Вам нужно переопределить эти функции в пространстве имен cereal, чтобы они не выполняли никаких операций и не вызывали ошибку.
#include <cassert>
#include <iostream>
#include <sstream>
#include <cereal/archives/json.hpp>
#include <cereal/types/vector.hpp>

namespace cereal {
    void prologue(cereal::JSONInputArchive&, const std::vector<int>&){}
    void epilogue(cereal::JSONInputArchive&, const std::vector<int>&){}
}
  1. Десериализация массива: Теперь вы можете написать основной код для десериализации вашего JSON-массива.
int main() {
    std::stringstream ss("[1,2,3]");
    cereal::JSONInputArchive ar(ss);
    std::vector<int> vec;

    ar(vec);  // Десериализация успешна

    assert(vec.size() == 3);  // Проверка корректности изменений
    std::cout << vec[0] << ", " << vec[1] << ", " << vec[2] << std::endl;  // Выводим элементы вектора
    return 0;
}

Заключение

Используя предложенные изменения, теперь вы можете без проблем десериализовать JSON-массивы в std::vector<int> с помощью библиотеки Cereal. Это решение позволяет избежать сложностей, связанных с ожиданием наличия ключ/значение в JSON-структуре и расширяет возможности работы с данными, получаемыми из API.

Библиотека Cereal в сочетании с такими изменениями открывает новые горизонты для разработки, позволяя обрабатывать чистые данные JSON, как это часто востребовано в современных приложениях и сервисах.

Если у вас будут возникать дополнительные вопросы по работе с Cereal или другими аспектами C++, не стесняйтесь обращаться за помощью.

Оцените материал
Добавить комментарий

Капча загружается...