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.

Author's profile. Photo taken in VRChat by my friend Tast+
Martin Chang
Systems software, HPC, GPGPU and AI. I mostly write stupid C++ code. Sometimes does AI research. Chronic VRChat addict

I run TLGS, a major search engine on Gemini. Used by Buran by default.


  • marty1885 \at protonmail.com
  • Matrix: @clehaxze:matrix.clehaxze.tw
  • Jami: a72b62ac04a958ca57739247aa1ed4fe0d11d2df