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.
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.
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 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.
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);
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.