4
\$\begingroup\$

I need a class to generate json content from data retrieved from a database. This gave me an excuse to play at creating a json library. This is my first attempt so it could probably be improved in many ways.

Features are:

  1. feed in json from stdin to populate internal structures.
  2. output json to stdout.
  3. insert into json.
  4. find by json key name.

It uses std::variant which requires c++17 or better.

Please review the code and give me some feedback.

Is it heading in the right direction?

Firstly the main header, json20.hpp:

#ifndef JSON20_HPP_
#define JSON20_HPP_

#include <variant>  // type-safe union
#include <string>
#include <vector>
#include <map>
#include <iostream>

#include "json_value.hpp"

struct json_array {
    std::vector<json_value> array;
};

struct json_object {
    std::map < std::string, json_value> object;

    json_value& operator [](std::string key) {
        return object[key];
    }
};

struct json_null {

};

std::ostream& operator<< (std::ostream& os, const json_value& v);


class json20 {
public:
    /* start empty constructor */
    json20() {}

    /* construct json object from input stream */
    json20(std::istream& istrm, std::string& parse_status);

    /* parent node of json object */
    json_value root;

    /* insert json object by manual construction. Returns true on successful insertion */
    bool insert(const json_object& object);

    /* find json value by key name */
    bool find_by_key(const std::string& key_name, json_value& value);

private:
    bool parse(std::istream& istrm, std::string& parse_status);
    json_value parse_json(std::istream& istrm, json_value& value, std::string& parse_status);
    bool recursive_find_by_key(const std::string& key_name, json_object& next, json_value& value);
};

#endif // JSON20_HPP_

json values header, json_value.hpp:

#ifndef JSON_VALUE_HPP_
#define JSON_VALUE_HPP_

#include "json20.hpp"

#include <variant>
#include <string>

// fwd declare
struct json_array;
struct json_object;
struct json_null;

typedef std::variant<double, bool, std::string, json_array, json_object, json_null> json_value;

#endif // JSON_VALUE_HPP_

json implementation file, json20.cpp:

#include "json20.hpp"

#include <cstring>

static void eat_whitespace(std::istream& istrm) {
    int ch;
    while (istrm) {
        ch = istrm.peek();
        if (isspace(ch)) {
            istrm.get();
        }
        else {
            break;
        }
    }
}

static char get_next_token(std::istream& istrm) {
    eat_whitespace(istrm);
    char ch;
    if (istrm.get(ch))
        return ch;
    else
        return (char)-1;  // indicate failure;
}

std::ostream& operator<< (std::ostream& os, const json_value& v) {

    switch (v.index()) {
    case 0:
        os << std::get<0>(v);
        break;
    case 1:
        if (std::get<1>(v)) {
            os << "true";
        }
        else {
            os << "false";
        }
        break;
    case 2:
        os << '"' << std::get<2>(v) << '"';
        break;
    case 3:
    {
        os << '[';
        bool first = true;
        json_array arr = std::get<json_array>(v);
        for (auto& item : arr.array) {
            if (!first) {
                os << ',';
            }
            os << item;
            first = false;
        }

        os << ']';
        break;
    }
    case 4:
    {
        os << '{';
        bool first = true;
        json_object obj = std::get<json_object>(v);
        for (auto& item : obj.object) {
            if (!first) {
                os << ',';
            }
            os << '"' << item.first << "\":";
            os << item.second;
            first = false;
        }

        os << '}';
        break;
    }
    case 5:
        os << "null";
        break;
    }

    return os;
}

static bool try_string(std::istream& istrm, json_value& value) {
    eat_whitespace(istrm);
    char ch = static_cast<char>(istrm.peek());
    if (ch != '"') {
        return false;
    }

    // remove first "
    istrm.get(ch);

    std::string s;
    while (istrm.get(ch)) {
        if (ch == '"') {
            value.emplace<2>(s);
            return true;
        }
        else {
            // haven't worked out how to just append ch to end of string in value
            s += ch;
        }
    }
    return false;
}

static bool try_number(std::istream& istrm, json_value& value) {

    eat_whitespace(istrm);
    char ch = static_cast<char>(istrm.peek());
    if (ch != '-' && !isdigit(ch)) {
        return false;
    }

    while (istrm.get(ch)) {
        // start of a number
        if (ch == '-' || (ch >= '0' && ch <= '9')) {
            char buf[10] = {};
            buf[0] = ch;
            int i = 1;
            while (istrm.get(ch)) {
                if (ch >= '0' && ch <= '9' || ch == '.') {
                    buf[i] = ch;
                    i++;
                }
                else {
                    istrm.putback(ch);
                    break;
                }
            }
            double number = strtod(buf, nullptr);
            value.emplace<0>(number);
            return true;
            break;
        }
        else {
            istrm.putback(ch);
            return false;
        }
    }
    return false;
}

static bool check_bytes(std::istream& istrm, const char* expected) {
    const size_t length(strlen(expected));
    for (size_t i = 0; i < length; i++) {
        char ch;
        if(!istrm.get(ch)) {
            return false;
        }
        if (ch != expected[i]) {
            return false;
        }
    }
    return true;
}

static bool try_boolean(std::istream& istrm, json_value& value, std::string& parse_status) {

    eat_whitespace(istrm);
    char ch = static_cast<char>(istrm.peek());
    if (ch != 'f' && ch != 't') {
        return false;
    }

    if (ch == 'f') {
        const char* expected = "false";
        if (check_bytes(istrm, expected)) {
            value.emplace<bool>(false);
            return true;
        }
        else {
            parse_status += "parse of boolean false value failed|";
            return false;
        }
    }
    if (ch == 't') {
        const char* expected = "true";
        if (check_bytes(istrm, expected)) {
            value.emplace<bool>(true);
            return true;
        }
        else {
            parse_status += "parse of boolean true value failed|";
            return false;
        }
    }

    return false;
}

static bool try_null(std::istream& istrm, json_value& value, std::string& parse_status) {

    eat_whitespace(istrm);
    char ch = static_cast<char>(istrm.peek());
    if (ch != 'n') {
        return false;
    } else {
        const char* expected = "null";
        if (check_bytes(istrm, expected)) {
            value.emplace<json_null>();
            return true;
        }
        else {
            parse_status += "parse of null value failed|";
            return false;
        }
    }
}

json20::json20(std::istream& istrm, std::string& parse_status) {
    parse(istrm, parse_status);
}

json_value json20::parse_json(std::istream& istrm, json_value& value, std::string& parse_status) {

    if (try_number(istrm, value)) {
        return value;
    }

    if (try_string(istrm, value)) {
        return value;
    }

    if (try_boolean(istrm, value, parse_status)) {
        return value;
    }

    if (try_null(istrm, value, parse_status)) {
        return value;
    }

    char ch = get_next_token(istrm);
    if (ch == '{') {

        json_object object;
        ch = get_next_token(istrm);
        if (ch == '}') {
            return object;
        }

        while (istrm) {
            if (ch == '"') {
                istrm.putback(ch);
                std::string key;
                json_value kvalue;

                if (!try_string(istrm, kvalue)) {
                    parse_status += "unexpected json parsing failure of name|";
                    return kvalue;
                }
                else {
                    key = std::get<2>(kvalue);
                }

                ch = get_next_token(istrm);
                if (ch != ':') {
                    parse_status += "encountered an unexpected symbol: ";
                    parse_status += ch;
                    parse_status += ", : was expected|";

                }

                object[key] = parse_json(istrm, value, parse_status);

                ch = get_next_token(istrm);
                if (ch == '}') {
                    return object;
                }

                if (ch != ',') {
                    parse_status += "encountered an unexpected symbol: ";
                    parse_status += ch;
                    parse_status += " , (comma) was expected|";
                    return object;
                }

                ch = get_next_token(istrm);
            }
            else {
                parse_status += "encountered an unexpected symbol: ";
                parse_status += ch;
                parse_status += " , \" (quote) was expected|";
                return object;
            }
        }
    }

    if (ch == '[') {
        json_array jarray;
        ch = get_next_token(istrm);
        if (ch == ']') {
            return jarray;
        }
        istrm.putback(ch);
        while (istrm) {
            jarray.array.push_back(parse_json(istrm, value, parse_status));
            ch = get_next_token(istrm);
            if (ch == ']') {
                break;
            }
            if (ch != ',') {
                parse_status += "comma expected, instead saw ";
                parse_status += ch;
                parse_status += '|';

            }
        }
        return jarray;
    }

    // if  get to here something is wrong
    parse_status += "parse failure, last character parsed: ";
    parse_status += ch;
    parse_status += '|';
    return value;  // no type/value will be set if return here
}

bool json20::parse(std::istream& istrm, std::string& parse_status) {

    root = parse_json(istrm, root, parse_status);

    return true;
}

bool json20::insert(const json_object& object) {

    root = object;

    return true;
}

bool json20::recursive_find_by_key(const std::string& key_name, json_object& next, json_value& value) {

    const auto found = next.object.find(key_name);
    if (found != next.object.end()) {
        value = found->second;
        return true;
    }
    // if not found, iterate over current values for child objects
    for (const auto element : next.object) {
        switch (element.second.index()) {
        case 3: // json_array
        {
            json_array jarray = std::get<json_array>(element.second);
            for (const auto list_item : jarray.array) {
                if (list_item.index() == 4) {
                    json_object child = std::get<json_object>(list_item);
                    return recursive_find_by_key(key_name, child, value);
                }
            }
        }
        break;
        case 4:  // json_object
            json_object child = std::get<json_object>(element.second);
            return recursive_find_by_key(key_name, child, value);
            break;
        }
    }
    return false;
}

bool json20::find_by_key(const std::string& key_name, json_value& value) {

    // root should be a json_object, but check just in case
    switch (root.index()) {
        case 4: // json_object
        {
            json_object obj = std::get<json_object>(root);
            if (recursive_find_by_key(key_name, obj, value)) {
                return true;
            }
            break;
        }
        default:
        break;
    }

    return false;
}

test code using gtest, test.cpp:

#include <gtest/gtest.h>

#include "json20.hpp"

#include <string>
#include <iostream>
#include <sstream>

TEST(json20, GivenEmptyJsonStringThenConstructsCorrectly) {

    std::string myjson = "{}";

    std::stringstream iostrm;
    iostrm << myjson;

    // take in through stdin
    std::string error;
    json20 json(iostrm, error);

    // output to stdout
    std::ostringstream ostrm;
    ostrm << json.root;

    std::string expected("{}");
    EXPECT_EQ(ostrm.str(), expected);
}

TEST(json20, GivenJsonStringUsingNullThenConstructsCorrectly) {

    std::string myjson = "{ \"myvalue\": null }";

    std::stringstream iostrm;
    iostrm << myjson;

    // take in through stdin
    std::string error;
    json20 json(iostrm, error);

    // output to stdout
    std::ostringstream ostrm;
    ostrm << json.root;

    std::string expected("{\"myvalue\":null}");
    EXPECT_EQ(ostrm.str(), expected);
}

TEST(json20, GivenNumberJsonStringThenConstructsCorrectly) {

    std::string myjson = "{ \"mynumber\": 3.142 }";

    std::stringstream iostrm;
    iostrm << myjson;

    // take in through stdin
    std::string error;
    json20 json(iostrm, error);

    // output to stdout
    std::ostringstream ostrm;
    ostrm << json.root;

    std::string expected("{\"mynumber\":3.142}");
    EXPECT_EQ(ostrm.str(), expected);
}

TEST(json20, GivenStringJsonStringThenConstructsCorrectly) {

    std::string myjson = "{ \"mystring\": \"Angus\" }";

    std::stringstream iostrm;
    iostrm << myjson;

    std::string error;
    json20 json(iostrm, error);

    std::ostringstream ostrm;
    ostrm << json.root;

    std::string expected("{\"mystring\":\"Angus\"}");
    EXPECT_EQ(ostrm.str(), expected);
}

TEST(json20, GivenStringJsonArrayThenConstructsCorrectly) {

    std::string myjson = "{ \"myarray\": [\"Angus\", \"Lisa\"]}";

    std::stringstream iostrm;
    iostrm << myjson;
    std::string error;
    json20 json(iostrm, error);

    std::ostringstream ostrm;
    ostrm << json.root;

    std::string expected("{\"myarray\":[\"Angus\",\"Lisa\"]}");
    EXPECT_EQ(ostrm.str(), expected);
}

TEST(json20, GivenNumberJsonArrayThenConstructsCorrectly) {

    std::string myjson = "{ \"myarray\": [1, 2, 3]}";

    std::stringstream iostrm;
    iostrm << myjson;
    std::string error;
    json20 json(iostrm, error);

    std::ostringstream ostrm;
    ostrm << json.root;

    std::string expected("{\"myarray\":[1,2,3]}");
    EXPECT_EQ(ostrm.str(), expected);
}

TEST(json20, GivenAMixedJsonArrayThenConstructsCorrectly) {

    std::string myjson = "{ \"myarray\": [1, \"Angus\", 3]}";

    std::stringstream iostrm;
    iostrm << myjson;
    std::string error;
    json20 json(iostrm, error);

    std::ostringstream ostrm;
    ostrm << json.root;

    std::string expected("{\"myarray\":[1,\"Angus\",3]}");
    EXPECT_EQ(ostrm.str(), expected);
}

TEST(json20, GivenMultipleJsonArraysThenConstructsCorrectly) {

    std::string myjson = "{ \"myarray\": [\"Angus\", \"Lisa\", {\"objage\": [true, 4,false]}, \"Wookie\"]}";

    std::stringstream iostrm;
    iostrm << myjson;
    std::string error;
    json20 json(iostrm, error);

    std::ostringstream ostrm;
    ostrm << json.root;

    std::string expected("{\"myarray\":[\"Angus\",\"Lisa\",{\"objage\":[true,4,false]},\"Wookie\"]}");
    EXPECT_EQ(ostrm.str(), expected);
}


TEST(json20, GivenComplexJsonThenConstructsCorrectly) {
    std::string myjson = R"###({
        "firstName": "John",
        "lastName" : "Smith",
        "isAlive" : true,
        "age" : 27,
        "address" : {
        "streetAddress": "21 2nd Street",
            "city" : "New York",
            "state" : "NY",
            "postalCode" : "10021-3100"
    },
        "phoneNumbers": [
        {
            "type": "home",
                "number" : "212 555-1234"
        },
    {
        "type": "office",
        "number" : "646 555-4567"
    }
        ],
            "children": [],
            "spouse": null
})###";

    std::stringstream iostrm;
    iostrm << myjson;
    std::string error;
    json20 json(iostrm, error);

    std::ostringstream ostrm;
    ostrm << json.root;

    std::string expected("{\"myarray\":[\"Angus\",\"Lisa\",{\"objage\":[true,4,false]},\"Wookie\"]}");
}


// failed cases
TEST(json20, GivenEmptyStringThenConstructsCorrectly) {

    std::string myjson;  // blank string

    std::stringstream iostrm;
    iostrm << myjson;

    // take in through stdin
    std::string error;
    json20 json(iostrm, error);

    // output to stdout
    std::ostringstream ostrm;
    ostrm << json.root;

    std::string expected("");
    EXPECT_NE(error.size(), 0u);
}

TEST(json20, GivenInvalidUnquotedStringValueJsonStringThenNoCrash) {

    std::string myjson = "{ \"mynumber\":unquoted string }";

    std::stringstream iostrm;
    iostrm << myjson;

    // take in through stdin
    std::string error;
    json20 json(iostrm, error);

    // output to stdout
    std::ostringstream ostrm;
    ostrm << json.root;

    EXPECT_NE(error.size(), 0u);
}

TEST(json20, GivenLargeButInvalidJsonStringThenNoCrash) {

    std::string myjson = R"###({
        "firstName": "John",
        "lastName" -- "Smith",
        "isAlive" : true,
        "age" : 27,
        "address" : {
        "streetAddress": "21 2nd Street",
            "city" : "New York",
            "state" : "NY",
            "postalCode" : "10021-3100"
    },
        "phoneNumbers": [
        {
            "type": "home",
                "number" : "212 555-1234"
        },
    {
        "type": "office",
        "number" : 646 555-4567
    }
        ],
            "children": [[[],
            "spouse": nill
})###";

    std::stringstream iostrm;
    iostrm << myjson;

    // take in through stdin
    std::string error;
    json20 json(iostrm, error);

    // output to stdout
    std::ostringstream ostrm;
    ostrm << json.root;

    EXPECT_NE(error.size(), 0u);
}

TEST(json20, GivenAJsonObjectThenOutputAsExpected) {

    const std::string s = "my list";
    double arrayofdoubles[] = {1.0, 2.0, 3.0};
    json_array jarray;
    for (auto i : arrayofdoubles) {
        jarray.array.push_back(i);
    }

    json_object obj;
    obj.object[s] = jarray;

    json20 json;
    json.insert(obj);

    //// output to stdout
    std::ostringstream ostrm;
    ostrm << json.root;

    std::string expected("{\"my list\":[1,2,3]}");
    EXPECT_EQ(ostrm.str(), expected);
}

TEST(json20, GivenValidJsonThenFindValueByKey) {
    std::string myjson = R"###({
        "firstName": "John",
        "lastName" : "Smith",
        "isAlive" : true,
        "age" : 27,
        "address" : {
        "streetAddress": "21 2nd Street",
            "city" : "New York",
            "state" : "NY",
            "postalCode" : "10021-3100"
    },
        "phoneNumbers": [
        {
            "type": "home",
                "number" : "212 555-1234"
        },
    {
        "type": "office",
        "number" : "646 555-4567"
    }
        ],
            "children": [],
            "spouse": null
})###";

    std::stringstream iostrm;
    iostrm << myjson;
    std::string error;
    json20 json(iostrm, error);

    json_value value;
    EXPECT_TRUE(json.find_by_key("firstName", value));

    const json_value expected = std::string("John");
    EXPECT_EQ(std::get<std::string>(value), std::get<std::string>(expected));
}

TEST(json20, GivenJsonWithNestedKeyThenFindValueByKey) {
    std::string myjson = R"###({
        "firstName": "John",
        "lastName" : "Smith",
        "nestedLevel1Key" : { "name": "childString1", "NestedLevel2Key": { "age": 10, "address1": "1 Woodlough Way" } }
    })###";

    std::stringstream iostrm;
    iostrm << myjson;
    std::string error;
    json20 json(iostrm, error);

    json_value value;
    EXPECT_TRUE(json.find_by_key("address1", value));

    const json_value expected = std::string("1 Woodlough Way");
    EXPECT_EQ(std::get<std::string>(value), std::get<std::string>(expected));
}

TEST(json20, GivenJsonWithNestedKeyInArrayThenFindValueByKey) {
    std::string myjson = R"###({
        "firstName": "John",
        "lastName" : "Smith",
        "nestedLevel1Key" : [ "age": 29, { "name": "childString1", "NestedLevel2Key": [{ "address1": "1 Woodlough Way" }] }]
    })###";

    std::stringstream iostrm;
    iostrm << myjson;
    std::string error;
    json20 json(iostrm, error);

    json_value value;
    EXPECT_TRUE(json.find_by_key("address1", value));

    const json_value expected = std::string("1 Woodlough Way");
    EXPECT_EQ(std::get<std::string>(value), std::get<std::string>(expected));
}

TEST(json20, GivenInvalidbooleanJsonValueThenParseErrorShouldIndicateBooleanError) {
    std::string myjson = R"###({
        "firstName": "John",
        "lastName" : "Smith",
        "isAlive" : truw,
        "age" : 27,
        "address" : {
        "streetAddress": "21 2nd Street",
            "city" : "New York",
            "state" : "NY",
            "postalCode" : "10021-3100"
    },
        "phoneNumbers": [
        {
            "type": "home",
                "number" : "212 555-1234"
        },
    {
        "type": "office",
        "number" : "646 555-4567"
    }
        ],
            "children": [],
            "spouse": null
})###";

    std::stringstream iostrm;
    iostrm << myjson;
    std::string error;
    json20 json(iostrm, error);

    size_t found_error = error.find("bool");
    EXPECT_NE(error.size(), 0u);
    EXPECT_NE(found_error, std::string::npos);
}
\$\endgroup\$
  • \$\begingroup\$ Have you see this? github.com/Loki-Astari/ThorsSerializer I had exactly the same issue. So I developed this. The idea was to convert to/from JSON without using an intermediate structure. \$\endgroup\$ – Martin York Apr 4 at 23:29
2
\$\begingroup\$

Code Review:

I don't like this as it gives you an extra level of indirection.

struct json_array {
    std::vector<json_value> array;
};

You can simply use another name:

using json_array = std::vector;

This gives you a specific name for the array and removes the level of indirection.


Sure this is resonable:

struct json_object {
    std::map < std::string, json_value> object;

Note That if you try and access an element that does not exist it will add it to the object (even if you are just reading).

    json_value& operator [](std::string key) {
        return object[key];

Maybe this is desirable, depends on your use case.

    }
};

But sometimes you pass by const reference. In this case you can not access members of the object because there is no const access to members. I would a way to accesses elements from a const reference.

  json_value const& operator [](std::string key) const {
        auto find object.find(key);
        if (find != object.end()) {
            return find->second;
        }
        // Not sure what you want to do if the object does not exist.
  }

Not really sure what json20 is for?

class json20 {

You don't need it to hold the JSON that is what json_value is for. To me this is JSON parsers, which is fine but you don't need to store the json_value inside. Personally I would rename this to JsonParser and then use to read a stream that returns a json_value object.


The stream operator >> drops all leading white space so you don't actually need to write your own.

static void eat_whitespace(std::istream& istrm) {

Personally I would write eat_whitespace like this:

static void eat_whitespace(std::istream& istrm) {

    int ch;
    while (istrm >> ch && std::isspace(ch)) {
        // Do nothing
    }
    if (istrm) {
        istrm.unget();  // We read a non whitespace. Put it back.
    }
}

So because operator>> drops leading white space we can re-write get_next_token

static char get_next_token(std::istream& istrm) {
    char ch = static_cast<char>(-1);
    istrm >> ch;  // Don't care if it does not work.
                  // If the read fails then `ch` is unmodified.
                  // So it will have a value of -1.
    return ch;
}

OK. Streaming:

std::ostream& operator<< (std::ostream& os, const json_value& v) {

For boolean values we can simplify it:

        // IF you want to do it manually.
        os << (std::get<1>(v)) ? "true" : "false";

        // Using the stream operator.
        os << std::boolalpha << std::get<1>(v);

For objects like the json_object, json_arry and json_null I would write their own stream operators.

    std::ostream& operator<< (std::ostream& os, json_array const& v);
    std::ostream& operator<< (std::ostream& os, json_object const& v);
    std::ostream& operator<< (std::ostream& os, json_null const& v);

Now you can simplify case 3/4/5:

    case 3:  os << std::get<3>(v); break;
    case 4:  os << std::get<4>(v); break;
    case 5:  os << std::get<5>(v); break; 

Lets simplify the try_string()

static bool try_string(std::istream& istrm, json_value& value) {

    char ch;
    if (istrm >> ch) {
        if (ch != '"') {
            istrm.unget();
            return false;
        }

        std::string s;
        std::getline(istrm, s, '"');
        value.emplace<2>(s);
        return true;
    }
    return false;
}

Pretty sure your try_number does not conform to the JSON standard.

static bool try_number(std::istream& istrm, json_value& value) {

https://www.json.org/json-en.html

  • Your code will read a number with multiple . in it.
  • Numbers can not start with 0 (unless it is just zero or zero with a fraction).
  • You don't support E suffix to numbers.

Personally I would convert the C++ code for parsing JSON into LEX code to parse JSON values. The code for lex is a lot simpler:

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ Thanks for the feedback, brilliant. As for the using json_array = std::vector; I assume you mean using json_array = std::vector<json_value>; I am struggling with the circular dependency. I can forward declare struct json_array, but I don't think you can fwd declare using directive? \$\endgroup\$ – arcomber Apr 5 at 9:11
  • 1
    \$\begingroup\$ The greatest benefit I got from this was: Not really sure what json20 is for? Absolutely right and I need to switch on my big picture brain! \$\endgroup\$ – arcomber Apr 5 at 10:57

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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