Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(De-)Serialization of std::variant with namespaces #2208

Closed
wbitesi opened this issue Jun 22, 2020 · 3 comments
Closed

(De-)Serialization of std::variant with namespaces #2208

wbitesi opened this issue Jun 22, 2020 · 3 comments

Comments

@wbitesi
Copy link

wbitesi commented Jun 22, 2020

I have problems with (de-)serialization with std::variant<...> if the variant types are in different namespaces (see the minimal example below, just compile with -std=c++17 for std::variant). If i put the types in the global namespace, the variant part just works. Anyone an idea how to use namespaces in the variant with json deserialization?

#include <exception>
#include <iostream>
#include <nlohmann/json.hpp>
#include <variant>

using json = nlohmann::json;
using std::cout;
using std::endl;

namespace foo
{

struct Foobar
{
  std::string foo;
};

void to_json(json &obj, const Foobar &foo)
{
  obj["type"] = "foo::Foobar";
  obj["foo"] = foo.foo;
}

void from_json(const json &obj, Foobar &foo)
{
  auto t = obj["type"].get<std::string>();
  if (t != "foo::Foobar")
    throw std::invalid_argument("property 'type' must be foo::Foobar");
  foo.foo = obj["foo"].get<std::string>();
}

} // namespace foo

namespace bar
{

struct Foobar
{
  std::string bar;
};

void to_json(json &obj, const Foobar &foo)
{
  obj["type"] = "bar::Foobar";
  obj["bar"] = foo.bar;
}

void from_json(const json &obj, Foobar &foo)
{
  auto t = obj["type"].get<std::string>();
  if (t != "bar::Foobar")
    throw std::invalid_argument("property 'type' must be bar::Foobar");
  foo.bar = obj["bar"].get<std::string>();
}

} // namespace bar

typedef typename std::variant<foo::Foobar, bar::Foobar> FoobarVariant;

void to_json(json &obj, const FoobarVariant &foo)
{
  if (std::holds_alternative<foo::Foobar>(foo))
    to_json(obj, std::get<foo::Foobar>(foo));
  else if (std::holds_alternative<bar::Foobar>(foo))
    to_json(obj, std::get<bar::Foobar>(foo));
  else
    throw std::runtime_error("non exhaustive pattern match in void "
                             "to_json(json &obj, const FoobarVariant &foo)");
}

void from_json(const json &obj, FoobarVariant &foo)
{
  auto t = obj["type"].get<std::string>();
  if (t == "foo::Foobar")
    foo::from_json(obj, std::get<foo::Foobar>(foo));
  else if (t == "bar::Foobar")
    bar::from_json(obj, std::get<bar::Foobar>(foo));
  else
    throw std::runtime_error("non exhaustive pattern match in void "
                             "from_json(const json &obj, FoobarVariant &foo)");
}

int main()
{
  const json json_foo_foobar = R"(
{
  "type": "foo::Foobar",
  "foo": "hello"
}
)"_json;
  foo::Foobar foo_foobar = json_foo_foobar.get<foo::Foobar>();
  cout << "foo::Foobar::foo = " << foo_foobar.foo << endl;

  const json json_bar_foobar = R"(
{
  "type": "bar::Foobar",
  "bar": "bar"
}
)"_json;
  auto bar_foobar = json_bar_foobar.get<bar::Foobar>();
  cout << "bar::Foobar::bar = " << bar_foobar.bar << endl;

  auto variant = json_foo_foobar.get<FoobarVariant>();
  if (!std::holds_alternative<foo::Foobar>(variant))
    throw std::runtime_error("but it should be a foo::Foobar");

  return 0;
}
@wbitesi
Copy link
Author

wbitesi commented Jun 26, 2020

I have found a solution for the problem. If the adl_serializer is specified with the variant type instead of providing from_json/to_json functions, the compiler is happy. Why the approach with from_json/to_json does not work, i cannot extract from the documentation (https://github.com/nlohmann/json#arbitrary-types-conversions)

namespace nlohmann
{
template <> struct adl_serializer<FoobarVariant>
{

  static void to_json(json &obj, const FoobarVariant &foo)
  {
    if (std::holds_alternative<foo::Foobar>(foo))
      foo::to_json(obj, std::get<foo::Foobar>(foo));
    else if (std::holds_alternative<bar::Foobar>(foo))
      bar::to_json(obj, std::get<bar::Foobar>(foo));
    else
      throw std::runtime_error("non exhaustive pattern match in void "
                               "to_json(json &obj, const FoobarVariant &foo)");
  }

  static void from_json(const json &obj, FoobarVariant &foo)
  {
    auto t = obj["type"].get<std::string>();
    if (t == "foo::Foobar")
      foo::from_json(obj, std::get<foo::Foobar>(foo));
    else if (t == "bar::Foobar")
      bar::from_json(obj, std::get<bar::Foobar>(foo));
    else
      throw std::runtime_error(
        "non exhaustive pattern match in void "
        "from_json(const json &obj, FoobarVariant &foo)");
  }
};

@wbitesi wbitesi closed this as completed Jun 26, 2020
@krishna116
Copy link

Thank you @wbitesi , it is a more concise method than my implementation.

@gregmarr
Copy link
Contributor

Another alternative: #3436 (comment)

For anyone that finds this in the future, and has this same question:

Why the approach with from_json/to_json does not work, i cannot extract from the documentation (https://github.com/nlohmann/json#arbitrary-types-conversions)

the answer is here: https://github.com/nlohmann/json#how-do-i-convert-third-party-types

This serializer works fine when you have control over the type's namespace. However, what about boost::optional or std::filesystem::path (C++17)? Hijacking the boost namespace is pretty bad, and it's illegal to add something other than template specializations to std...

To solve this, you need to add a specialization of adl_serializer to the nlohmann namespace

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants