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