Understanding JSON → C++
Header-only, macro-driven, surprisingly ergonomic.
Why nlohmann/json became the de-facto C++ JSON library, and the ADL trick that makes struct-to-JSON look like operator overloading magic.
nlohmann/json is the default choice.
The de-facto C++ JSON library is Niels Lohmann's single-header nlohmann/json. Header-only (drop the file in, no build-system surgery), modern (C++17 minimum), and unusually ergonomic for C++. Alternatives like RapidJSON are faster and lower-allocation but require more careful usage; for almost all application code, the default choice is nlohmann/json.
NLOHMANN_JSON_SERIALIZE — the macro.
The library defines a macro, NLOHMANN_DEFINE_TYPE_INTRUSIVE, that generates the to_json and from_json free functions for your struct in one line. ADL (argument-dependent lookup) then makes json j = my_struct; work automatically, just like an overloaded operator. The codegen output is a struct definition followed by one macro invocation per struct. Concise and unobtrusive.
Optional types for nullable fields.
C++17's std::optional<T> is the right type for a JSON field that might be missing or null. The library round-trips it correctly: a present, non-null value populates the optional; a missing or null field leaves it empty. Without optional, a missing field in the input causes a parse error (or worse, a default-constructed value masquerading as real data). Codegen tools should default optional on every nullable-by-schema field.
A worked example.
From { "user_id": 7, "name": "Q", "avatar": null }: #include <nlohmann/json.hpp>
#include <optional>
#include <string>
struct User {
int user_id;
std::string name;
std::optional<std::string> avatar;
};
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(User, user_id, name, avatar);
The struct fields use snake_case to match the JSON keys (or use the _WITH_DEFAULT variant for explicit name maps). The macro generates the serialization functions. json j = u; and User u2 = j; both work.
One struct + one macro
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(T, fields...)
The macro emits to_json/from_json free functions for the struct.
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(User, user_id, name, avatar);
= Round-trippable type
INTRUSIVE vs NON_INTRUSIVE.
Two variants of the macro. NLOHMANN_DEFINE_TYPE_INTRUSIVE goes inside the struct definition and needs access to private members. NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE goes outside the struct and only works on public members. Use intrusive when fields are private; non-intrusive when they're public (and you'd rather not change the struct itself).
Custom types and free functions.
For more complex types — enums, chrono durations, third-party types — write your own to_json / from_json overloads in the same namespace as the type. ADL finds them automatically. The library doesn't care whether the implementation came from a macro or a hand-written function; the contract is the two functions exist.
Performance — when to look elsewhere.
nlohmann/json is fast enough for most use cases but allocates more than it needs to. For hot paths in performance-critical code (serializing millions of records per second, parsing huge files), RapidJSON's SAX-style API or simdjson's vectorised parser can be 5-20× faster. The trade-off is ergonomics: those libraries require manual node traversal rather than struct-to-JSON automation. Default to nlohmann; switch only when profiling says you must.