How to serialize a json object without enclosing it in a sub-object using Cereal
Asked Answered
A

3

7

Let's say I have a class in C++, like the following:

struct Point {
    int x, y, z;
};

I want to use Cereal to serialize that struct to JSON. So I added a serialize function like this:

struct Point {
    int x, y, z;
    template<class Archive>
    void serialize(Archive& ar) {
        ar(CEREAL_NVP(x),
           CEREAL_NVP(y),
           CEREAL_NVP(z));
    }
};

This works fine when the Point is a member of another object or an element of an array. But if I want to make the Point be the primary object of an entire JSON file, it doesn't work properly. For example, with the following code:

Point p { 1, 2, 3 };
cereal::JSONOutputArchive ar(std::cout);
ar(p);

I get the following output:

{
    "value0": {
        "x": 1,
        "y": 2,
        "z": 3
    }
}

I'd like to remove the "value0" key and elevate the object to occupy the entire file, like this:

{
    "x": 1,
    "y": 2,
    "z": 3
}

The only way I can seem to do that, is to basically re-implement the serialization function, manually adding the key names.

Point p {1, 2, 3};
cereal::JSONOutputArchive ar(std::cout);
ar(cereal::make_nvp("x", p.x),
   cereal::make_nvp("y", p.y),
   cereal::make_nvp("z", p.z));

Is there any way to do it utilizing the serialize function that I already implemented for the class?

Accelerant answered 15/11, 2015 at 22:54 Comment(0)
A
10

Okay, figured it out. Pretty simple, just needed to call the serialize function directly from the object, passing the archive, instead of passing the object to the archive.

Point p {1, 2, 3};
cereal::JSONOutputArchive ar(std::cout);
p.serialize(ar);
Accelerant answered 15/11, 2015 at 23:6 Comment(2)
Unfortunately, this solution doesn't work for serializing const objects, because the serialize method can't be const.Gurley
The fact that I had to find this answer just speaks about how terrible cereal documentation and samples are.Pilotage
S
4

You need to define the epilogue and prologue functions for your own type to be no-ops, so that the ones from the chosen archive won't be selected instead.

#include <cereal/archives/json.hpp>
#include <iostream>

struct Point {
    int x, y, z;
    template<class Archive>
    void serialize(Archive& ar) {
        ar(CEREAL_NVP(x),
           CEREAL_NVP(y),
           CEREAL_NVP(z));
    }
};

void epilogue(cereal::JSONOutputArchive&, const Point &){}
void prologue(cereal::JSONOutputArchive&, const Point &){}

int main()
{
    Point p { 1, 2, 3 };
    cereal::JSONOutputArchive ar(std::cout);
    ar(p); 

    return 0;
}
Stannic answered 1/5, 2020 at 3:49 Comment(5)
Perfect! This is the cleanest solution for me.Gurley
I thought that templates could be used to accomplish this for all structs, but then the compiler complains about the ambiguous epilogue() / prologue().Gurley
Your solution works for serializing an object to JSON string, but when I want to deserialize it back from string to an object, it complains with rapidjson internal assertion failure: IsObject(). When I put back the enclosing value0 node into the JSON string, deserialization works again. So is this solution incompatible with deserialization?Gurley
@MichalFapso, if you want to de-serialize as well, the epilogue and prologue functions should be adapted for the JSONInputArchive too.Stannic
Ooooh, of course, thanks for pointing that out, @Fabio A. :)Gurley
P
2

Benjamin's answer is perfect solution if you know upfront that the class to be serialized has a serialize() method. Since Cereal supports in-class/out-of-class serialize(), split load()/save(), explicit versioning; that's not always the case. Cereal's internal cereal::InputArchive and cereal::OutputArchive classes both have a bunch of SFINAE template methods to detect the right serialization method to use during compile time. The type-traits there can be used to roll our own template switch:

template< typename Class, typename Archive,
          typename std::enable_if< cereal::traits::has_member_serialize<Class, Archive>::value>::type* = nullptr>
inline static void serializeHelper(Class& cl, Archive& ar)
{
    cl.serialize(ar);
}

template< typename Class, typename Archive,
          typename std::enable_if< cereal::traits::has_member_save<Class, Archive>::value>::type* = nullptr>
inline static void serializeHelper(Class& cl, Archive& ar)
{
    cl.save(ar);
}

// More version could follow for remaining serialization types (external, versioned...)

template< typename Class, typename Archive,
          typename std::enable_if< cereal::traits::has_member_serialize<Class, Archive>::value>::type* = nullptr>
inline static void deserializeHelper(Class& cl, Archive& ar)
{
    cl.serialize(ar);
}

template< typename Class, typename Archive,
          typename std::enable_if< cereal::traits::has_member_load<Class, Archive>::value>::type* = nullptr>
inline static void deserializeHelper(Class& cl, Archive& ar)
{
    cl.load(ar);
}

// More version could follow for remaining deserialization types (external, versioned...)

Calling serializeHelper(p, ar); will automatically select the serialization method provided by Point, the same way as Cereal does internally.

Pittman answered 18/5, 2016 at 10:11 Comment(1)
Even this solution falls short: it won't work with const objects & output archives.Gessner

© 2022 - 2024 — McMap. All rights reserved.