2

Hi Stack Overflow Community !

I am working on a project that heavily uses the interesting nlohmann_json library and it appears that I need to add an inheritance link on a specific class, which objects are serialized at one moment.

I tried different advice found on the github Issues page of the library, but can't make it work.

Here is an dummy code I tried :

#include <nlohmann/json.hpp>

#include <iostream>
#include <memory>
#include <vector>

using json = nlohmann::json;

namespace nlohmann {
    template <typename T>
    struct adl_serializer<std::unique_ptr<T>> {
        static void to_json(json& j, const std::unique_ptr<T>& opt) {
            if (opt) {
                j = *opt.get();
            } else {
                j = nullptr;
            }
        }
    };
}

class Base {
    public:
        Base() = default;
        virtual ~Base() = default;
        virtual void foo() const { std::cout << "Base::foo()" << std::endl; }
};

class Obj : public Base
{
    public:
        Obj(int i) : _i(i) {}
        void foo() const override { std::cout << "Obj::foo()" << std::endl; }
        int _i = 0;
        friend std::ostream& operator<<(std::ostream& os, const Obj& o);
};

std::ostream& operator<<(std::ostream& os, const Base& o)
{
    os << "Base{} ";
    return os;
}

std::ostream& operator<<(std::ostream& os, const Obj& o)
{
    os << "Obj{"<< o._i <<"} ";
    return os;
}

void to_json(json& j, const Base& b)
{
    std::cout << "called to_json for Base" << std::endl;
}

void to_json(json& j, const Obj& o)
{
    std::cout << "called to_json for Obj" << std::endl;
}

int main()
{
    std::vector<std::unique_ptr<Base>> v;
    v.push_back(std::make_unique<Base>());
    v.push_back(std::make_unique<Obj>(5));
    v.push_back(std::make_unique<Base>());
    v.push_back(std::make_unique<Obj>(10));

    std::cout << v.size() << std::endl;

    json j = v;
}
// Results in :
// Program returned: 0
// 4
// called to_json for Base
// called to_json for Base
// called to_json for Base
// called to_json for Base

(https://gcc.godbolt.org/z/dc8h8f)

I understand that the adl_serializer only get the type Base when called, but I don't see how to make him aware of the type Obj as well...

Does anyone see what I am missing here ?

Thanks in advance for your advice and help !

mscherer
  • 97
  • 1
  • 3
  • 12
  • You will have to implement your own polymorphic layer, for example by storing an additional `type` field inside the JSON and checking its value when deserializing. – Quentin Oct 14 '20 at 08:39
  • @Quentin I understand that you are talking about the management of polymorphism inside a JSON-formated file while de-/serializing, and totally agree with that. But here, the problem is that I can't serialize my vector containing polymorphic objects. I don't know why or how to make `adl_serializer` call the function `void to_json(json& j, const Obj& o) for `Obj`-typed objects instead of the function `void to_json(json& j, const Base& b)`. – mscherer Oct 14 '20 at 09:20
  • I don't see a virtual destructor. Bad. – Asteroids With Wings Oct 14 '20 at 12:42
  • @AsteroidsWithWings Yep, you're right, it should. It is just a dummy code per se, but you're right :) – mscherer Oct 14 '20 at 14:26
  • Dummy code or not, you have undefined behaviour and (though I guess we've ruled that out given the accepted answer below) that sort of thing can lead to the sort of problem you've encountered :) Never skip your virtual destructor. – Asteroids With Wings Oct 14 '20 at 14:45

1 Answers1

6

nlohmann.json does not include polymorphic serializing, but you can implement it yourself in a specialized adl_serializer. Here we're storing and checking an additional _type JSON field, used as a key to map to pairs of type-erased from/to functions for each derived type.

namespace PolymorphicJsonSerializer_impl {
    template <class Base>
    struct Serializer {
        void (*to_json)(json &j, Base const &o);
        void (*from_json)(json const &j, Base &o);
    };

    template <class Base, class Derived>
    Serializer<Base> serializerFor() {
        return {
            [](json &j, Base const &o) {
                return to_json(j, static_cast<Derived const &>(o));
            },
            [](json const &j, Base &o) {
                return from_json(j, static_cast<Derived &>(o));
            }
        };
    }
}

template <class Base>
struct PolymorphicJsonSerializer {

    // Maps typeid(x).name() to the from/to serialization functions
    static inline std::unordered_map<
        char const *,
        PolymorphicJsonSerializer_impl::Serializer<Base>
    > _serializers;

    template <class... Derived>
    static void register_types() {
        (_serializers.emplace(
            typeid(Derived).name(),
            PolymorphicJsonSerializer_impl::serializerFor<Base, Derived>()
        ), ...);
    }

    static void to_json(json &j, Base const &o) {
        char const *typeName = typeid(o).name();
        _serializers.at(typeName).to_json(j, o);
        j["_type"] = typeName;
    }

    static void from_json(json const &j, Base &o) {
        _serializers.at(j.at("_type").get<std::string>().c_str()).from_json(j, o);
    }
};

Usage:

// Register the polymorphic serializer for objects derived from `Base`
namespace nlohmann {
    template <>
    struct adl_serializer<Base>
        : PolymorphicJsonSerializer<Base> { };
}

// Implement `Base`'s from/to functions
void to_json(json &, Base const &) { /* ... */ }
void from_json(json const &, Base &) { /* ... */ }


// Later, implement `Obj`'s from/to functions
void to_json(json &, Obj const &) { /* ... */ }
void from_json(json const &, Obj &) { /* ... */ }

// Before any serializing/deserializing of objects derived from `Base`, call the registering function for all known types.
PolymorphicJsonSerializer<Base>::register_types<Base, Obj>();

// Works!
json j = v;

Caveats:

  • typeid(o).name() is unique in practice, but is not guaranteed to be by the standard. If this is an issue, it can be replaced with any persistent runtime type identification method.

  • Error handling has been left out, though _serializers.at() will throw std::out_of_range when trying to serialize an unknown type.

  • This implementation requires that the Base type implements its serialization with ADL from/to functions, since it takes over nlohmann::adl_serializer<Base>.

See it live on Wandbox

Quentin
  • 62,093
  • 7
  • 131
  • 191
  • This was absolutely not what I understand if your first comment, my apology... and it is brilliant ! I did not thought about that. It is not obvious so, maybe even out of league for now, and for this, thank you a lot ! – mscherer Oct 14 '20 at 12:38
  • Wouldn't it be better to use `std::string` for the keys in the map instead of `const char *`? In my understanding the lookup when using `const char *` would compare the pointers, not the values. Also, this might be the reason why the above example does not work when deserializing values. – Cereaubra Jan 16 '22 at 07:37
  • @Cereaubra that's what `typeid(o).name()` [returns](https://en.cppreference.com/w/cpp/types/type_info/name), and it should constitute a reasonably good unique identifier as far as I can tell. Can you expand on what doesn't work? – Quentin Jan 18 '22 at 09:29
  • When modifing your wandbox.com example to also deserialize an exception is thrown: `terminate called after throwing an instance of 'std::out_of_range'. what(): _Map_base::at`. I added the following three lines: `Base b1 = *v[0].get();` `json j_b1 = b1;` `Base re_b1 = j_b1;` The exception is thrown at the lookup from `_serializers` via `j.at("_type")` – Cereaubra Jan 18 '22 at 13:38
  • 1
    This works nicely for converting to a `json` object, but how would converting back to a `vector` work? Do we need to write a factory function which reads the `_type` property and creates the appropriate subclass before calling `*sub_ptr = j.get`? – Tim Meyer Apr 09 '22 at 19:31