C++ JSON Serialization

C++ JSON Serialization

本文关键字:Serialization JSON C++      更新时间:2023-10-16

我想要一种方法,尽可能自动地将对象序列化和反序列化为 JSON。

序列 化:对我来说,理想的方法是,如果我在实例中调用 JSONSerialize((,它会返回一个带有 JSON 对象的字符串,该对象具有该对象的所有公共属性"name_of_property": "value"。对于那些原语值,这很简单,对于对象,它应该尝试调用每个 JSONSerialize(( 或 ToString(( 或类似的东西来递归序列化所有公共属性。对于集合,它也应该行为正确(只有向量/数组就可以了(。

反序列化:只需创建给定对象(假设一只狗(的实例并调用 JSONDeserialize(json_string) ,这应该填充所有公共属性,在属性不是原语或所需集合的情况下创建所需的对象。

一个示例应该像这样运行:

Dog *d1 = new Dog();
d1->name = "myDog";
string serialized = d1->JSONSerialize();
Dog *d2 = new Dog();
d2->JSONDeserialize(serialized);
std::cout << d2->name; // This will print "myDog"

或者像这样:

Dog *d1 = new Dog();
d1->name = "myDog";
string serialized = JSONSerializer.Serialize(d1);
Dog *d2 = JSONSerializer.Deserialize(serialized, Dog);
std::cout << d2->name; // This will print "myDog"

我怎样才能轻松做到这一点?

C++中没有反射。真。但是,如果编译器无法为您提供所需的元数据,您可以自己提供。

让我们从创建一个属性结构开始:

template<typename Class, typename T>
struct PropertyImpl {
    constexpr PropertyImpl(T Class::*aMember, const char* aName) : member{aMember}, name{aName} {}
    using Type = T;
    T Class::*member;
    const char* name;
};
template<typename Class, typename T>
constexpr auto property(T Class::*member, const char* name) {
    return PropertyImpl<Class, T>{member, name};
}

当然,您也可以有一个采用资源库和 getter 而不是指向成员的指针的property,并且可能只读取要序列化的计算值的属性。如果使用 C++17,则可以进一步扩展它以创建适用于 lambda 的属性。

好的,现在我们有了编译时自省系统的构建块。

现在在您的类Dog 中,添加您的元数据:

struct Dog {
    std::string barkType;
    std::string color;
    int weight = 0;
    
    bool operator==(const Dog& rhs) const {
        return std::tie(barkType, color, weight) == std::tie(rhs.barkType, rhs.color, rhs.weight);
    }
    
    constexpr static auto properties = std::make_tuple(
        property(&Dog::barkType, "barkType"),
        property(&Dog::color, "color"),
        property(&Dog::weight, "weight")
    );
};

我们需要对该列表进行迭代。要迭代元组,有很多方法,但我更喜欢这样的方法:

template <typename T, T... S, typename F>
constexpr void for_sequence(std::integer_sequence<T, S...>, F&& f) {
    using unpack_t = int[];
    (void)unpack_t{(static_cast<void>(f(std::integral_constant<T, S>{})), 0)..., 0};
}

如果编译器中有 C++17 折表达式可用,则可以将for_sequence简化为:

template <typename T, T... S, typename F>
constexpr void for_sequence(std::integer_sequence<T, S...>, F&& f) {
    (static_cast<void>(f(std::integral_constant<T, S>{})), ...);
}

这将为整数序列中的每个常量调用一个函数。

如果此方法不起作用或给编译器带来麻烦,则始终可以使用数组扩展技巧。

现在,您已经有了所需的元数据和工具,可以循环访问属性以取消序列化:

// unserialize function
template<typename T>
T fromJson(const Json::Value& data) {
    T object;
    // We first get the number of properties
    constexpr auto nbProperties = std::tuple_size<decltype(T::properties)>::value;
    
    // We iterate on the index sequence of size `nbProperties`
    for_sequence(std::make_index_sequence<nbProperties>{}, [&](auto i) {
        // get the property
        constexpr auto property = std::get<i>(T::properties);
        // get the type of the property
        using Type = typename decltype(property)::Type;
        // set the value to the member
        // you can also replace `asAny` by `fromJson` to recursively serialize
        object.*(property.member) = Json::asAny<Type>(data[property.name]);
    });
    return object;
}

对于序列化:

template<typename T>
Json::Value toJson(const T& object) {
    Json::Value data;
    // We first get the number of properties
    constexpr auto nbProperties = std::tuple_size<decltype(T::properties)>::value;
    
    // We iterate on the index sequence of size `nbProperties`
    for_sequence(std::make_index_sequence<nbProperties>{}, [&](auto i) {
        // get the property
        constexpr auto property = std::get<i>(T::properties);
        // set the value to the member
        data[property.name] = object.*(property.member);
    });
    return data;
}

如果需要递归序列化和反序列化,可以将asAny替换为 fromJson

现在你可以像这样使用你的函数:

Dog dog;
dog.color = "green";
dog.barkType = "whaf";
dog.weight = 30;
Json::Value jsonDog = toJson(dog); // produces {"color":"green", "barkType":"whaf", "weight": 30}
auto dog2 = fromJson<Dog>(jsonDog);
std::cout << std::boolalpha << (dog == dog2) << std::endl; // pass the test, both dog are equal!

做!不需要运行时反思,只需要一些 C++14 的好处!

此代码可以从一些改进中受益,当然可以通过一些调整与 C++11 一起使用。

请注意,需要编写 asAny 函数。它只是一个函数,它接受Json::Value并调用正确的as...函数或其他fromJson

这是一个完整的工作示例,由此答案的各种代码片段组成。随意使用它。

如注释中所述,此代码不适用于 msvc。如果您想要兼容的代码,请参考这个问题: 指向成员的指针:适用于 GCC

div class="answers>为此

,您需要在 C/C++ 中进行反射,而 C/中不存在。您需要一些元数据来描述类(成员、继承的基类(的结构。目前,C/C++编译器不会在构建的二进制文件中自动提供该信息。

也有同样的想法,我使用GCC XML项目来获取这些信息。它输出描述类结构的 XML 数据。我已经建立了一个项目,我正在解释此页面中的一些关键点:

序列化很容易,但我们必须处理复杂的数据结构实现(例如 std::string、std::map(,这些实现使用分配的缓冲区。反序列化更复杂,您需要重建对象及其所有成员,以及对 vtables 的引用......一个痛苦的实现。

例如,您可以像这样序列化:

    // Random class initialization
    com::class1* aObject = new com::class1();
    for (int i=0; i<10; i++){
            aObject->setData(i,i);
    }      
    aObject->pdata = new char[7];
    for (int i=0; i<7; i++){
            aObject->pdata[i] = 7-i;
    }
    // dictionary initialization
    cjson::dictionary aDict("./data/dictionary.xml");
    // json transformation
    std::string aJson = aDict.toJson<com::class1>(aObject);
    // print encoded class
    cout << aJson << std::endl ;

要反序列化数据,它的工作原理如下:

    // decode the object
    com::class1* aDecodedObject = aDict.fromJson<com::class1>(aJson);
    // modify data
    aDecodedObject->setData(4,22);
    // json transformation
    aJson = aDict.toJson<com::class1>(aDecodedObject);
   
    // print encoded class
    cout << aJson << std::endl ;

>:~/cjson$ ./main
{"_index":54,"_inner":  {"_ident":"test","pi":3.141593},"_name":"first","com::class0::_type":"type","com::class0::data":[0,1,2,3,4,5,6,7,8,9],"com::classb::_ref":"ref","com::classm1::_type":"typem1","com::classm1::pdata":[7,6,5,4,3,2,1]}
{"_index":54,"_inner":{"_ident":"test","pi":3.141593},"_name":"first","com::class0::_type":"type","com::class0::data":[0,1,2,3,22,5,6,7,8,9],"com::classb::_ref":"ref","com::classm1::_type":"typem1","com::classm1::pdata":[7,6,5,4,3,2,1]}
>:~/cjson$ 

通常这些实现依赖于编译器(例如ABI规范(,并且需要外部描述才能工作(GCCXML输出(,因此并不容易集成到项目中。

这样容易的事情存在吗?谢谢:)(

C++ 不会在编译的代码中存储类成员名称,并且无法(在运行时(发现类包含哪些成员(变量/方法(。换句话说,不能循环访问结构的成员。由于没有这样的机制,因此无法为每个对象自动创建"JSONserialize"。

但是,您可以使用任何 json 库来序列化对象,但您必须为每个类自己编写序列化/反序列化代码。 要么这样,要么你必须创建类似于 QVariantMap 的可序列化类,该类将用于所有可序列化对象的结构。

换句话说,如果您愿意对所有可序列化对象使用特定类型(或者为每个类自己编写序列化例程(,则可以这样做。但是,如果要自动序列化每个可能的类,则应忘记它。如果此功能对您很重要,请尝试使用其他语言。

使用快速类型,可以从 JSON 示例数据生成C++序列化程序和反序列化程序。

例如,给定示例 JSON:

{
  "breed": "Boxer",
  "age": 5,
  "tail_length": 6.5
}

快速类型生成:

#include "json.hpp"
namespace quicktype {
    using nlohmann::json;
    struct Dog {
        int64_t age;
        std::string breed;
        double tail_length;
    };

    inline json get_untyped(const json &j, const char *property) {
        if (j.find(property) != j.end()) {
            return j.at(property).get<json>();
        }
        return json();
    }
}
namespace nlohmann {
    inline void from_json(const json& _j, struct quicktype::Dog& _x) {
        _x.age = _j.at("age").get<int64_t>();
        _x.breed = _j.at("breed").get<std::string>();
        _x.tail_length = _j.at("tail_length").get<double>();
    }
    inline void to_json(json& _j, const struct quicktype::Dog& _x) {
        _j = json{{"age", _x.age}, {"breed", _x.breed}, {"tail_length", _x.tail_length}};
    }
}

要解析 Dog JSON 数据,请包含上面的代码,安装 Boost 和 json.hpp,然后执行以下操作:

Dog dog = nlohmann::json::parse(jsonString);

如果有人仍然有这个需求(我有(,我自己写了一个库来处理这个问题。看这里。它不是完全自动的,因为您必须描述类中的所有字段,但它与我们可以得到的一样接近C++缺乏反射。

试试json_dto。它仅是标题且易于使用。

简单的例子:

struct message_t
{
  std::string m_from;
  std::string m_text;
  
  // Entry point for json_dto.
  template < typename JSON_IO >
  void
  json_io( JSON_IO & io )
  {
    io
      & json_dto::mandatory( "from", m_from )
      & json_dto::mandatory( "text", m_text );
  }
};

这将可 JSON 相互转换:

{ "from" : "json_dto", "text" : "Hello world!" }

jsoncons C++仅标头库还支持 JSON 文本和C++对象之间的转换。 解码和编码是为已定义json_type_traits的所有类C++定义的。标准库容器已受支持,json_type_traits可以专门用于 jsoncons 命名空间中的用户类型。

下面是一个示例:

#include <iostream>
#include <jsoncons/json.hpp>
namespace ns {
    enum class hiking_experience {beginner,intermediate,advanced};
    class hiking_reputon
    {
        std::string rater_;
        hiking_experience assertion_;
        std::string rated_;
        double rating_;
    public:
        hiking_reputon(const std::string& rater,
                       hiking_experience assertion,
                       const std::string& rated,
                       double rating)
            : rater_(rater), assertion_(assertion), rated_(rated), rating_(rating)
        {
        }
        const std::string& rater() const {return rater_;}
        hiking_experience assertion() const {return assertion_;}
        const std::string& rated() const {return rated_;}
        double rating() const {return rating_;}
    };
    class hiking_reputation
    {
        std::string application_;
        std::vector<hiking_reputon> reputons_;
    public:
        hiking_reputation(const std::string& application, 
                          const std::vector<hiking_reputon>& reputons)
            : application_(application), 
              reputons_(reputons)
        {}
        const std::string& application() const { return application_;}
        const std::vector<hiking_reputon>& reputons() const { return reputons_;}
    };
} // namespace ns
// Declare the traits using convenience macros. Specify which data members need to be serialized.
JSONCONS_ENUM_TRAITS_DECL(ns::hiking_experience, beginner, intermediate, advanced)
JSONCONS_ALL_CTOR_GETTER_TRAITS(ns::hiking_reputon, rater, assertion, rated, rating)
JSONCONS_ALL_CTOR_GETTER_TRAITS(ns::hiking_reputation, application, reputons)
using namespace jsoncons; // for convenience
int main()
{
std::string data = R"(
    {
       "application": "hiking",
       "reputons": [
       {
           "rater": "HikingAsylum",
           "assertion": "advanced",
           "rated": "Marilyn C",
           "rating": 0.90
         }
       ]
    }
)";
    // Decode the string of data into a c++ structure
    ns::hiking_reputation v = decode_json<ns::hiking_reputation>(data);
    // Iterate over reputons array value
    std::cout << "(1)n";
    for (const auto& item : v.reputons())
    {
        std::cout << item.rated() << ", " << item.rating() << "n";
    }
    // Encode the c++ structure into a string
    std::string s;
    encode_json<ns::hiking_reputation>(v, s, indenting::indent);
    std::cout << "(2)n";
    std::cout << s << "n";
}    

输出:

(1)
Marilyn C, 0.9
(2)
{
    "application": "hiking",
    "reputons": [
        {
            "assertion": "advanced",
            "rated": "Marilyn C",
            "rater": "HikingAsylum",
            "rating": 0.9
        }
    ]
}

使用 ThorsSerializer

Dog *d1 = new Dog();
d1->name = "myDog";
std::stringstream  stream << ThorsAnvil::Serialize::jsonExport(d1);
string serialized = stream.str();
Dog *d2 = nullptr;
stream >> ThorsAnvil::Serialize::jsonImport(d2);
std::cout << d2->name; // This will print "myDog"

我认为这与您的原版非常接近。
有一点设置。您需要声明您的类是可序列化的。

#include "ThorSerialize/Traits.h"
#include "ThorSerialize/JsonThor.h"
struct Dog
{
    std::string  name;
};
// Declare the "Dog" class is Serializable; Serialize the member "name"
ThorsAnvil_MakeTrait(Dog, name);

不需要其他编码。

可以找到完整的示例:

  • 简单示例
  • 容器示例
  • 多态示例

尚未提及,尽管这是我搜索结果中的第一个:https://github.com/nlohmann/json

列出的福利:

  • 直观的语法(看起来很棒!
  • 要包含的单个头文件,没有其他内容
  • 可笑的测试

此外,它还在 MIT 许可证下。

老实说:我还没有使用它,但通过一些经验,我有一个诀窍来确定我何时遇到一个制作精良的 c++ 库。

这是我

使用Qt:https://github.com/carlonluca/lqobjectserializer 的尝试。像这样的 JSON:

{"menu": {
    "header": "SVG Viewer",
    "items": [
        {"id": "Open"},
        {"id": "OpenNew", "label": "Open New"},
        null,
        {"id": "ZoomIn", "label": "Zoom In"},
        {"id": "ZoomOut", "label": "Zoom Out"},
        {"id": "OriginalView", "label": "Original View"},
        null,
        {"id": "Quality"},
        {"id": "Pause"},
        {"id": "Mute"},
        null,
        {"id": "Find", "label": "Find..."},
        {"id": "FindAgain", "label": "Find Again"},
        {"id": "Copy"},
        {"id": "CopyAgain", "label": "Copy Again"},
        {"id": "CopySVG", "label": "Copy SVG"},
        {"id": "ViewSVG", "label": "View SVG"},
        {"id": "ViewSource", "label": "View Source"},
        {"id": "SaveAs", "label": "Save As"},
        null,
        {"id": "Help"},
        {"id": "About", "label": "About Adobe CVG Viewer..."}
    ]
}}

可以通过声明如下类来反序列化:

L_BEGIN_CLASS(Item)
L_RW_PROP(QString, id, setId, QString())
L_RW_PROP(QString, label, setLabel, QString())
L_END_CLASS
L_BEGIN_CLASS(Menu)
L_RW_PROP(QString, header, setHeader)
L_RW_PROP_ARRAY_WITH_ADDER(Item*, items, setItems)
L_END_CLASS
L_BEGIN_CLASS(MenuRoot)
L_RW_PROP(Menu*, menu, setMenu, nullptr)
L_END_CLASS

和写作写作:

LDeserializer<MenuRoot> deserializer;
QScopedPointer<MenuRoot> g(deserializer.deserialize(jsonString));

您还需要为元对象注入一次映射:

QHash<QString, QMetaObject> factory {
    { QSL("Item*"), Item::staticMetaObject },
    { QSL("Menu*"), Menu::staticMetaObject }
};

我正在寻找一种避免这种情况的方法。