danielaparker / jsoncons

A C++, header-only library for constructing JSON and JSON-like data formats, with JSON Pointer, JSON Patch, JSON Schema, JSONPath, JMESPath, CSV, MessagePack, CBOR, BSON, UBJSON
https://danielaparker.github.io/jsoncons
Other
720 stars 163 forks source link

std::variant as vocabulary type #257

Closed rbroggi closed 4 years ago

rbroggi commented 4 years ago

Dear Daniel,

While using the library I've realized that it seems the lib does not have built-in support for std::variant (as it has for std::optional for example). I think it could be a handy feature to support something like the piece of code below:

class ItemTypeOne {
 public:
  const std::string& GetId() const {
    return _id;
  }
  const std::string& GetContent() const {
    return _content;
  }

 private:
  JSONCONS_TYPE_TRAITS_FRIEND;
  std::string _id;
  std::string _content;
};

class ItemTypeTwo {
 public:
  const std::string& GetName() const {
    return _name;
  }

 private:
  JSONCONS_TYPE_TRAITS_FRIEND;
  std::string _name;
};

class Request {
 public:
  const std::optional<std::string>& GetDomain() const {
    return _domain;
  }
  const std::vector<std::variant<acv::ItemTypeOne, acv::ItemTypeTwo>>& GetItems() const {
    return _items;
  }

 private:
  JSONCONS_TYPE_TRAITS_FRIEND;
  std::optional<std::string> _domain;
  std::vector<std::variant<acv::ItemTypeOne, acv::ItemTypeTwo>> _items;
};

JSONCONS_ALL_MEMBER_NAME_TRAITS(acv::ItemTypeOne, (_id, "id"), (_content, "content"));
JSONCONS_ALL_MEMBER_NAME_TRAITS(acv::ItemTypeTwo, (_name, "name"));
JSONCONS_ALL_MEMBER_NAME_TRAITS(acv::Request, (_items, "item"), (_domain, "domain") );

Having std::variant instead of 'polimorphism' has several advantages and I would love seeing jsoncons integrating it as a vocabulary type.

Thank you once again for the amazing product and job,

Rodrigo

danielaparker commented 4 years ago

Could you provide some sample JSON which match your classes, that I can use for a test case?

Thanks, Daniel

rbroggi commented 4 years ago

Hi Daniel, sure!

So let's assume first this cpp structure:

enum class Color { YELLOW, RED, GREEN, BLUE };
JSONCONS_ENUM_NAME_TRAITS(Color, (YELLOW, "YELLOW"), (RED, "RED"), (GREEN, "GREEN"), (BLUE, "BLUE"));

class Fruit {
 private:
  JSONCONS_TYPE_TRAITS_FRIEND;
  std::string name;
  Color color;
};
JSONCONS_ALL_MEMBER_NAME_TRAITS(Fruit,
                                (name, "name"),
                                (color, "color"));

class Indument {
 private:
  JSONCONS_TYPE_TRAITS_FRIEND;
  int size;
  std::string material;
};
JSONCONS_ALL_MEMBER_NAME_TRAITS(Indument,
                                (size, "size"),
                                (material, "material"));

class Basket {
 private:
  JSONCONS_TYPE_TRAITS_FRIEND;
  std::string owner;
  std::vector<std::variant<Fruit, Basket>> items;
};
JSONCONS_ALL_MEMBER_NAME_TRAITS(Basket,
                                (owner, "owner"),
                                (items, "items"));

I would expect the following json to be compliant (deserializable):

{
  "owner": "Rodrigo",
  "items": [
    {
      "name": "banana",
      "color": "YELLOW"
    },
    {
      "size": 40,
      "material": "wool"
    },
    {
      "name": "apple",
      "color": "RED"
    },
    {
      "size": 40,
      "material": "cotton"
    }
  ]
}

and the following json to be non-compliant:

{
  "owner": "Rodrigo",
  "items": [
    {
      "name": "banana",
      "color": "YELLOW"
    },
    {
      "size": 40,
      "color": "RED"
    },
    {
      "tittle": "what a feature!"
    },
    {
      "size": 40,
      "material": "cotton"
    }
  ]
}

Basically I would be able to have either Indument or Fruit in my array. Of course the idea would be to have variant working with:

does that help?

Thank you, Rodrigo

danielaparker commented 4 years ago

This feature is now supported on master provided that C++17 is detected.

#include <jsoncons/json.hpp>

namespace ns {

    enum class Color {yellow, red, green, blue};

    inline
    std::ostream& operator<<(std::ostream& os, Color val)
    {
        switch (val)
        {
            case Color::yellow: os << "yellow"; break;
            case Color::red: os << "red"; break;
            case Color::green: os << "green"; break;
            case Color::blue: os << "blue"; break;
        }
        return os;
    }

    class Fruit 
    {
    private:
        JSONCONS_TYPE_TRAITS_FRIEND
        std::string name_;
        Color color_;
    public:
        friend std::ostream& operator<<(std::ostream& os, const Fruit& val)
        {
            os << "name: " << val.name_ << ", color: " << val.color_ << "\n";
            return os;
        }
    };

    class Fabric 
    {
    private:
        JSONCONS_TYPE_TRAITS_FRIEND
        int size_;
        std::string material_;
    public:
        friend std::ostream& operator<<(std::ostream& os, const Fabric& val)
        {
            os << "size: " << val.size_ << ", material: " << val.material_ << "\n";
            return os;
        }
    };

    class Basket 
    {
    private:
        JSONCONS_TYPE_TRAITS_FRIEND
        std::string owner_;
        std::vector<std::variant<Fruit, Fabric>> items_;

    public:
        std::string owner() const
        {
            return owner_;
        }

        std::vector<std::variant<Fruit, Fabric>> items() const
        {
            return items_;
        }
    };

} // ns

JSONCONS_ENUM_NAME_TRAITS(ns::Color, (yellow, "YELLOW"), (red, "RED"), (green, "GREEN"), (blue, "BLUE"))

JSONCONS_ALL_MEMBER_NAME_TRAITS(ns::Fruit,
                                (name_, "name"),
                                (color_, "color"))
JSONCONS_ALL_MEMBER_NAME_TRAITS(ns::Fabric,
                                (size_, "size"),
                                (material_, "material"))
JSONCONS_ALL_MEMBER_NAME_TRAITS(ns::Basket,
                                (owner_, "owner"),
                                (items_, "items"))

int main()
{
    std::string input = R"(
{
  "owner": "Rodrigo",
  "items": [
    {
      "name": "banana",
      "color": "YELLOW"
    },
    {
      "size": 40,
      "material": "wool"
    },
    {
      "name": "apple",
      "color": "RED"
    },
    {
      "size": 40,
      "material": "cotton"
    }
  ]
}
    )";

    ns::Basket basket = jsoncons::decode_json<ns::Basket>(input);
    std::cout << basket.owner() << "\n\n";

    std::cout << "(1)\n";
    for (const auto& var : basket.items()) 
    {
        std::visit([](auto&& arg) {
            using T = std::decay_t<decltype(arg)>;
            if constexpr (std::is_same_v<T, ns::Fruit>)
                std::cout << "Fruit " << arg << '\n';
            else if constexpr (std::is_same_v<T, ns::Fabric>)
                std::cout << "Fabric " << arg << '\n';
        }, var);
    }

    std::string output;
    jsoncons::encode_json(basket, output, jsoncons::indenting::indent);
    std::cout << "(2)\n" << output << "\n\n";
}

Output:

Rodrigo

(1)
Fruit name: banana, color: yellow

Fabric size: 28, material: wool

Fruit name: apple, color: red

Fabric size: 28, material: cotton

(2)
{
    "items": [
        {
            "color": "YELLOW",
            "name": "banana"
        },
        {
            "material": "wool",
            "size": 40
        },
        {
            "color": "RED",
            "name": "apple"
        },
        {
            "material": "cotton",
            "size": 40
        }
    ],
    "owner": "Rodrigo"
}

For classes supported through the convenience macros, e.g. Fruit and Fabric, the type selection strategy is the same as for polymorphic types, and is based on the presence of mandatory members in the classes. More generally, the type selection strategy is based on the json_type_traits<Json,T>::is(const Json& j) function, checking each type in the variant from left to right, and stopping when json_type_traits<Json,T>::is(j) returns true.

Now consider

int main()
{
    using variant_type  = std::variant<int, double, bool, std::string, ns::Color>;

    variant_type var1(100);
    variant_type var2(10.1);
    variant_type var3(false);
    variant_type var4(std::string("Hello World"));
    variant_type var5(ns::Color::yellow);

    std::string buffer1;
    jsoncons::encode_json(var1,buffer1);
    std::string buffer2;
    jsoncons::encode_json(var2,buffer2);
    std::string buffer3;
    jsoncons::encode_json(var3,buffer3);
    std::string buffer4;
    jsoncons::encode_json(var4,buffer4);
    std::string buffer5;
    jsoncons::encode_json(var5,buffer5);

    std::cout << "(1) " << buffer1 << "\n";
    std::cout << "(2) " << buffer2 << "\n";
    std::cout << "(3) " << buffer3 << "\n";
    std::cout << "(4) " << buffer4 << "\n";
    std::cout << "(5) " << buffer5 << "\n";

    auto v1 = jsoncons::decode_json<variant_type>(buffer1);
    auto v2 = jsoncons::decode_json<variant_type>(buffer2);
    auto v3 = jsoncons::decode_json<variant_type>(buffer3);
    auto v4 = jsoncons::decode_json<variant_type>(buffer4);
    auto v5 = jsoncons::decode_json<variant_type>(buffer5);

    auto visitor = [](auto&& arg) {
            using T = std::decay_t<decltype(arg)>;
            if constexpr (std::is_same_v<T, int>)
                std::cout << "int " << arg << '\n';
            else if constexpr (std::is_same_v<T, double>)
                std::cout << "double " << arg << '\n';
            else if constexpr (std::is_same_v<T, bool>)
                std::cout << "bool " << arg << '\n';
            else if constexpr (std::is_same_v<T, std::string>)
                std::cout << "std::string " << arg << '\n';
            else if constexpr (std::is_same_v<T, ns::Color>)
                std::cout << "ns::Color " << arg << '\n';
        };

    std::cout << "\n";
    std::cout << "(6) ";
    std::visit(visitor, v1);
    std::cout << "(7) ";
    std::visit(visitor, v2);
    std::cout << "(8) ";
    std::visit(visitor, v3);
    std::cout << "(9) ";
    std::visit(visitor, v4);
    std::cout << "(10) ";
    std::visit(visitor, v5);
    std::cout << "\n\n";
}

Output:

(1) 100
(2) 10.1
(3) false
(4) "Hello World"
(5) "YELLOW"

(6) int 100
(7) double 10.1
(8) bool false
(9) std::string Hello World
(10) std::string YELLOW

Encode is fine. But when decoding, jsoncons checks if the JSON string "YELLOW" is a std::string before it checks whether it is an ns::Color, and since the answer is yes, it goes into the variant as a std::string.

But if we switch the order of ns::Color and std::string in the variant definition, viz.

 using variant_type  = std::variant<int, double, bool, ns::Color, std::string>;

strings containing the text "YELLOW", "RED", "GREEN", or "BLUE" are detected to be ns::Color,and the others std::string.

And the output becomes

(1) 100
(2) 10.1
(3) false
(4) "Hello World"
(5) "YELLOW"

(6) int 100
(7) double 10.1
(8) bool false
(9) std::string Hello World
(10) ns::Color yellow

So: types that are more constrained should appear to the left of types that are less constrained.