3
\$\begingroup\$

As a personal challenge I want to implement a C++ JSON Parser. As part of that I have implemented the following Types/Data-structures and a way of querying them.

To represent the type of an individual JSON attribute I defined the following std::variant and aliased it as JSONType. My thought behind using an std::variant is that when dealing with a JSON attribute I know that it must be one of the following types, but I can't exactly know which type before parsing the attribute.

Right now I am not concerned about "null" and "arrays with different types".

using JSONType = std::variant
<
    bool,
    int,
    float,
    double,
    std::string,

    std::vector<bool>,
    std::vector<int>,
    std::vector<float>,
    std::vector<double>,
    std::vector<std::string>
>;

To represent a JSON object I defined the struct JSONObject. My reasoning behind the attributes member is that for every JSON attribute I have a string as it's key and the value is either a single JSONType (bool, int, ...) or another JSONObject that recursively repeats this structure.

The query function "getIf(keys)" expects a template type T, which is the type of data that the user expects to get out of the query. keys is a sequence of strings , where the first n-1 strings describe the path of nested JSONObjects down the tree to which the attribute resides that we want to return. So the nth string is the name of that attribute.

struct JSONObject
{
    std::unordered_map<std::string, std::variant<JSONType, JSONObject>> attributes;

    template <class T>
    T* getIf(std::vector<std::string> const& keys)
    {
        JSONObject* temp = this;

        // Go to JSONObject where last keys attribute resides.
        for (int i = 0; i < (keys.size() - 1); ++i)
        {
            temp = &std::get<JSONObject>(temp->attributes[keys[i]]);
        }

        // Find the attribute that we actually want to return,
        // which is the attribute that is pointed to by
        // the last given key.
        JSONType& variant = std::get<JSONType>(temp->attributes[keys[keys.size() - 1]]);

        // Check if the given template type T is the same type
        // that the attribute that we want to return has.
        if (auto* value = std::get_if<T>(&variant))
        {
            return value;
        }
        else
        {
            return nullptr;
        }
    }
};

The following is an example instatiation and query of a JSONObject that represents the following json file and should result in a tree-like structure like the diagram shows.

    JSONObject o
    { // Initialization brackets
        { // unordered_map brackets
            { "boolean", std::variant<JSONType, JSONObject>(true) }, // map entry brackets
            { "nested_object", std::variant<JSONType, JSONObject>(JSONObject
                {
                    {
                        { "float", std::variant<JSONType, JSONObject>(3.14123f)},
                        { "nested_object_2", std::variant<JSONType, JSONObject>(JSONObject
                            {
                                {
                                    { "string", std::variant<JSONType, JSONObject>(std::string("Hello World"))}
                                }
                            }
                        )},
                        { "numbers", std::variant<JSONType, JSONObject>(std::vector<int>{1, 2, 3, 4, 5}) }
                    }
                } 
            )}
        }
    };

    bool boolean = *o.getIf<bool>({ "boolean" });
    float flo = *o.getIf<float>({ "nested_object", "float" });
    std::string string = *o.getIf<std::string>({ "nested_object", "nested_object_2", "string" });
    std::vector<int> numbers = *o.getIf<std::vector<int>>({ "nested_object", "numbers" });

        {
            "boolean": true,
            "nested_object":
            {
                "float": 3.14123f,
                "nested_object_2":
                {
                    "string": "Hello World"
                },
                "numbers": [1, 2, 3, 4, 5]
            }
        }

Diagram

I am interested in the quality of this solution and alternative solutions. Thanks !

\$\endgroup\$
1

1 Answer 1

4
\$\begingroup\$

Don't like this:

using JSONType = std::variant
<
    bool,
    int,
    float,
    double,
    std::string,

    std::vector<bool>,
    std::vector<int>,
    std::vector<float>,
    std::vector<double>,
    std::vector<std::string>
>;

That's not really what the type looks like. The array (Vector) can have any JSON type as a member. I think a better version would be:

#include <string>
#include <unordered_map>
#include <vector>
#include <variant>

enum JsonType {Null, Obj, Vec, Bool, Int, Double, String};
class   Json;
struct JsonObj
{
    std::unordered_map<std::string, Json>   members;
};
using   JsonVec   = std::vector<Json>;
union JsonUnion
{
    JsonUnion() {null_ = nullptr;}
    ~JsonUnion(){}
    void*       null_;
    JsonObj     object_;
    JsonVec     array_;
    bool        bool_;
    int         int_;
    double      real_;
    std::string string_;
};
class Json
{

    JsonType     type;
    JsonUnion    data;
    public:
        Json()
            : type(Null)
            , data()
        {}
};

int main()
{
    Json    value;
}

The get function assumes you only have objects. You should be able to handle de-referencing arrays. But that requires two types of get parameter (integer and string).

using Access = std::variant<int, std::string>;

template <class T>
T* getIf(std::vector<Access> const& keys)

Also why are you returning a pointer?

T* getIf()

Memory management is hard. That is why C got such a bad reputation for being hard. Java tried to solve this with the garbage collector (which just caused more issues with runtime running). C++ solved the problem by introducing "Automated Fine Grain Deterministic Memory Management" also know as "Smart Pointers". This is how memory management is done consisely and reliabily in modern C++.

std::unqiue_ptr<T> getIf()

Using class in the template is a bit old school.

template <class T>
T* getIf(

Sure it is technically valid. But most people use typename. It has exactly the same meaning to the compiler. But to the human it implies that T can be any type (not just a class Type).


If you are "getting" something from an object then I would normally expect that this would not alter the object. I notice that your getIf() is not const. You probably did this because it does not compile with const. This is because you use operator[] on the unordered map.

temp = &std::get<JSONObject>(temp->attributes[keys[i]]);
                                             ^       ^

When looking a value in a unordered_map (or map) and you use the square braces then if the key does not exist it is added to the structure. This is probably not what you want.

I would change this so it uses a find. If the object does not have the appropriate key then you have a serious issue and I would throw an exception:

auto find = temp->attributes.find(keys[i]);
if (find == temp->attributes.end()) {
    throw "Bad Member Name";
}

temp = &std::get<JSONObject>(find->second);

\$\endgroup\$
4
  • \$\begingroup\$ For clarification, by "The get function assumes you only have objects. You should be able to handle de-referencing arrays. But that requires two types of get parameter (integer and string)." you mean that if I have the attribute "numbers": [1, 2, 3, 4, 5], I should be able to return an individual value of that array ? Maybe I misunderstand you. \$\endgroup\$ Commented Jun 11, 2020 at 8:58
  • 1
    \$\begingroup\$ @MoritzSchäfer: getIf<int>("nested_object", "numbers", 3) should be able to return the 3 element in "numbers" that is in the member "nested_object" inside your object. \$\endgroup\$ Commented Jun 11, 2020 at 18:09
  • \$\begingroup\$ You are using a union for JSONUnion instead of an std::variant because you can't do null = nullptr; with std::variant ? \$\endgroup\$ Commented Jun 12, 2020 at 7:08
  • \$\begingroup\$ @MoritzSchäfer I used a union because I don't know how to use std::variant it's not something I have found the need for yet. Note: I don't use union often either (about twice in thirty years). \$\endgroup\$ Commented Jun 12, 2020 at 8:12

Not the answer you're looking for? Browse other questions tagged or ask your own question.