Compare nlohmann/json to Glaze

I've been wanting to replace nlohmann/json with something else in my codebase for a while now. Recently Glaze entered my radar and I decided to give it a try. Here are my thoughts after writing a PoC program comparing the two libraries.

Performance

Glaze beats nlohmann/json. Just look at the numbers on glaze's README. Beating nlohmann is a pretty low bar so expected. Glaze is in fact almost as fast as RapidJSON.

Data type support

Both libraries supports elementry types that JSON asks for. `double`, `bool`, `string`, `array`, `object`, `null`. However nlohmann/json supports more types like `int` and `size_t` while Glaze only supports double (remember JavaScript only has a single Number type, which is actually IEEE 754 double precision floating point). So the following code is legal in nlohmann/json but not in Glaze:

// Fine with nlohmann/json
nlohmann::json j = nlohmann::json::parse("42");
int i = j.get<int>();

// Error with Glaze
glz::json_t j;
auto err glz::read_json(j, "42");
// int i = j.get<int>(); // BOOM! Compile error
int i = j.get<double>(); // OK

Which.. I understand that JSON doesn't support any integer type and you should always pass integers as strings. It still a bit terrifying to me that I can lose precision by using Glaze. There's more to this. Although both libraries supports getting arrays as `std::vector`. Glaze does not support assigning `std::vector` to a JSON object.

// Fine with nlohmann/json
nlohmann::json j = std::vector<int>{1, 2, 3};

// Blows up with Glaze
glz::json_t j = std::vector<int>{1, 2, 3};

Error handling

Error handling wise. Glaze is MUCH better then nlohmann/json. When accessing via a const reference nlohmann/json will simple UB if you try to access a key that doesn't exist. Less using the `at()` method. On the other hand, Glaze will throw an exception if you try to access a key that doesn't exist. I think this is the killer for nlohmann/json. I've had so many bugs in my codebase because of this. And could have been solved easily.

// UB with nlohmann/json
nlohmann::json j = nlohmann::json::parse("{\"one\": 1}");
const auto& json = j;
auto i = json["key"].get<int>(); // UB

// Exception with Glaze
glz::json_t j;
auto err = glz::read_json(j, "{\"one\": 1}");
const auto& json = j;
double i = json["key"].get<double>(); // Exception

The issue with nlohmann is as follows.

// in basic_json
const_reference operator[](const typename object_t::key_type& key) const
{
    // const operator[] only works for objects
    if (JSON_HEDLEY_LIKELY(is_object()))
    {
        auto it = m_value.object->find(key);
        JSON_ASSERT(it != m_value.object->end());
        return it->second;
    }

    JSON_THROW(type_error::create(305, detail::concat("cannot use operator[] with a string argument with ", type_name()), this));
}

// JSON_ASSERT is supposed to stop the program in case the key is not found
// but it is an no-op in release mode

#if !defined(JSON_ASSERT)
    #include <cassert> // assert
    #define JSON_ASSERT(x) assert(x)
#endif

I can override the `JSON_ASSERT` macro to do something else in release mode. But I really think that's what the library should have done in the first place.

Serialization/Deserialization

Glaze wins in this category. Glaze has built-in reflection support. So you can serialize and deserialize your classes without writing any boilerplate code. nlohmann/json has you writing your own serialization/deserialization functions. Which is a pain and I never bother to use it since in practice I'm only doing it at a single place.

struct MyStruct
{
    int a;
    double b;
    std::string c;
    std::vector<int> d;
};
MyStruct s{1, 2.0, "3", {4, 5, 6}};

// Glaze
std::string json = glz::write_json(s); // done. No special code

// nlohmann/json

void to_json(nlohmann::json& j, const MyStruct& s)
{
    j = nlohmann::json{
        {"a", s.a},
        {"b", s.b},
        {"c", s.c},
        {"d", s.d}
    };
}
std::string json = s;

However, there are points I need to complain about Glaze. Glaze, by default will error if the JSON string contains keys that are not in the struct. I feel this is a bad thing as APIs often add new keys to their responses. Since most languages ignore extra keys in JSON objects. I think Glaze should have an option to ignore extra keys. nlohmann/json follows the standard desearialization rules. It will ignore extra keys and not error.

std::string json_str = "{\"a\": 1, \"b\": 2.0, \"c\": \"3\", \"d\": [4, 5, 6], \"e\": 7}";
MyStruct s;
auto err = glz::read_json(s, json_str); // Error. e is not in MyStruct

This could be solved by using a SKIP meta attribute. But still I should be able to skip ALL extra keys. Even so, it is a whitlisting approach.

template <>
struct glz::meta<MyStruct>
{
    static constexpr auto value = object("a", &MyStruct::a, "b", &MyStruct::b, "c", &MyStruct::c, "d", &MyStruct::d, "e", skip{});
};

Instead a compile time option `glz::opts{.error_on_unknown_keys = false}>` must be set to ignore extra keys.

auto err = glz::read<glz::opts{.error_on_unknown_keys = false}>(s, json_str);

Conclusion

All n all I think glaze is quite cool and reflection support solves a huge chunk of my distaste and robustness issues with nlohmann/json. But due to the lack of support of integers and some (questionable) defaults, you may or may not like it.