Poperton
Poperton

Reputation: 2146

How to serialize/deserialize std::map<int, MyObject> to/from json in C++

I'm trying to serialize/deserialize std::map<int, MyObject> using https://github.com/nlohmann/json

It says here that the keys for the map must be able to generate std::string. How can I achieve that?

Basic usage for implementing conversion to/from my custom object MyObject would be:

using nlohmann::json;

namespace ns {
    void to_json(json& j, const MyObject& p) {
        j = json{{"name", p.name}, {"address", p.address}, {"age", p.age}};
    }

    void from_json(const json& j, MyObject& p) {
        j.at("name").get_to(p.name);
        j.at("address").get_to(p.address);
        j.at("age").get_to(p.age);
    }
} // namespace ns

however what should I do for the map? The last thing I want to do is implement void to_json(json& j, const std::map<int, MyObject>& p) and void from_json(const json& j, std::map<int, MyObject>& p) . Since the library already supports std::map, it should do everything for me, I just need to convert the int to std::string. How can I do just that?

Upvotes: 1

Views: 6524

Answers (2)

pcfist
pcfist

Reputation: 169

Here's a generic solution: define to_json() and from_json() function templates for arbitrary std::map as follows:

template <typename KTy, typename VTy> inline
void to_json(nlohmann::json& j, const std::map<KTy, VTy>& m)
{
    using std::to_string;
    for (const auto& e : m)
    {
        j[to_string(e.first)] = e.second;
    }
}

template <typename KTy, typename VTy> inline
void from_json(const nlohmann::json& j, std::map<KTy, VTy>& m)
{
    for (const auto& e : j.items())
    {
        KeyTy k;
        from_string(e.key(), k);
        auto v = e.value().get<VTy>();
        m[k] = v;
    }
}

Now we have define a way to convert string values to our map's key_type ("deserialize" the keys). For example, it can be achieved with a from_string() function template like this:

// A generic implementation (stub).
template <typename Ty> inline
void from_string(const std::string& key, Ty& value)
{
    value = key;
}

template <> inline
void from_string(const std::string& key, int& value)
{
    char* endPtr = nullptr;
    val = std::strtol(key.c_str(), &endPtr, 10);
}
// Add more specializations for other key types here.

Having these functions in place, and to/from_json() overloads for custom types (from the original question), one can convert std::map to and from JSON the natural way:

nlohmann::json j;
std::map<int, MyObject> myMap;
// ...
myMap = j.at("inputs").get<std::map<int, MyObject>>();
j = myMap;

Upvotes: 0

cdhowie
cdhowie

Reputation: 169028

The wording in the readme is clear as to why this doesn't work (emphasis mine):

Likewise, any associative key-value containers ... whose keys can construct an std::string and whose values can be used to construct JSON values ... can be used to create a JSON object.

This is why using int as a key doesn't work with this library; an std::string cannot be constructed from an int.


The natural follow-up question is why this isn't allowed. This is something you would have to ask the author of the library for a definitive answer.

We could speculate that it's because there is no one way to convert an integral to a string. Perhaps assume a base 10 representation with no leading zeros would be a reasonable choice, but there is no compelling reason why this must be the only choice.

What about hex-encoded strings, or scientific notation, or thousands separators, or any possible number of other options?

Another possible reason is that integral keys suggest a sparse array, so it may be unclear whether you are requesting an object or an array be produced in the output.


A possible solution would be to build a helper that converts any map to an std::map<std::string, TValue> by running the keys through std::to_string():

// Copy variant
template <typename T>
std::map<std::string, typename T::mapped_type> to_string_keyed_map(T const & input) {
    std::map<std::string, typename T::mapped_type> output;

    for (auto const & pair : input) {
        output.emplace(std::to_string(pair.first), pair.second);
    }

    return output;
}

// Move variant
template <typename T>
std::map<std::string, typename T::mapped_type> to_string_keyed_map(T && input) {
    std::map<std::string, typename T::mapped_type> output;

    for (auto & pair : input) {
        output.emplace(std::to_string(pair.first), std::move(pair.second));
    }

    return output;
}

Upvotes: 2

Related Questions