JSON in C++: nlohmann/json Parse, Serialize, Access & RapidJSON Comparison
Last updated:
C++ JSON handling centers on two libraries: nlohmann/json (header-only, expressive API) and RapidJSON (10× faster SAX streaming for large files). nlohmann/json is the most downloaded C++ JSON library — add a single #include <nlohmann/json.hpp> and json::parse(str) parses a JSON string into a strongly-typed nlohmann::json object. j.dump() serializes back to a compact string; j.dump(4) pretty-prints with 4-space indentation. Object construction uses initializer lists: json j = {{"name", "Alice"}, {"age", 30}}. Access fields with j["key"] (throws on missing key for const objects) or j.value("key", default_value) (returns the default). Serialize C++ structs automatically with NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(MyStruct, field1, field2). This guide covers json::parse, j.dump(), object/array construction, safe access patterns, iterating JSON objects and arrays, struct serialization macros, and when to choose RapidJSON over nlohmann/json.
Parsing JSON Strings with json::parse()
nlohmann/json is distributed as a single header file. Download json.hpp or install via vcpkg (vcpkg install nlohmann-json), Conan (nlohmann_json/3.11.3), or CMake FetchContent. The json::parse() static method accepts a std::string, a C string literal, or any std::istream and returns an nlohmann::json value. Invalid JSON throws json::parse_error; use the non-throwing overload to return a discarded value instead.
#include <nlohmann/json.hpp>
#include <iostream>
#include <stdexcept>
using json = nlohmann::json;
int main() {
// ── Basic parse from string ──────────────────────────────────
std::string raw = R"({"name":"Alice","age":30,"active":true})";
json j = json::parse(raw);
// Access values with typed .get<T>()
std::string name = j["name"].get<std::string>(); // "Alice"
int age = j["age"].get<int>(); // 30
bool ok = j["active"].get<bool>(); // true
std::cout << name << " (" << age << ")\n";
// ── Parse from C string literal ───────────────────────────────
json j2 = json::parse(R"([1, 2, 3, "four", null, true])");
// j2 is a JSON array — j2[0].get<int>() == 1
// ── Parse from std::istream ───────────────────────────────────
// std::istringstream ss(raw);
// json j3;
// ss >> j3; // equivalent to json::parse(ss)
// ── Throwing parse error ──────────────────────────────────────
try {
json bad = json::parse("{invalid json}");
} catch (const json::parse_error& e) {
// e.id — numeric error code (e.g. 101)
// e.byte — 1-based byte offset where error occurred
// e.what()— human-readable description
std::cerr << "Parse error at byte " << e.byte
<< ": " << e.what() << "\n";
}
// ── Non-throwing parse (returns discarded on failure) ─────────
json safe = json::parse("{bad}", nullptr, /*allow_exceptions=*/false);
if (safe.is_discarded()) {
std::cerr << "Parse failed\n";
}
// ── Parse NDJSON / JSON Lines — one object per line ───────────
std::string lines = R"({"id":1}
{"id":2}
{"id":3})";
std::istringstream ls(lines);
std::string line;
while (std::getline(ls, line)) {
json record = json::parse(line);
std::cout << record["id"].get<int>() << "\n";
}
return 0;
}The using json = nlohmann::json alias is idiomatic — all nlohmann/json code uses it. R"(...)" raw string literals eliminate the need to escape inner double quotes when embedding JSON in C++ source, making test fixtures and inline data clean to write. The library parses JSON numbers into C++ int, int64_t, uint64_t, or double depending on the value range and whether a decimal point is present — call j.is_number_integer() or j.is_number_float() to distinguish before calling get<T>().
Constructing JSON Objects and Arrays
nlohmann/json supports C++ initializer list construction for both objects and arrays. The library infers the type — if the initializer contains key-value pairs (pairs of string + value), it becomes a JSON object; otherwise it becomes a JSON array. You can also build JSON values incrementally using subscript assignment and push_back().
#include <nlohmann/json.hpp>
#include <vector>
#include <map>
using json = nlohmann::json;
int main() {
// ── JSON object from initializer list ────────────────────────
json person = {
{"name", "Alice"},
{"age", 30},
{"active", true},
{"score", 98.5},
{"alias", nullptr} // JSON null
};
// Serializes to: {"active":true,"age":30,"alias":null,"name":"Alice","score":98.5}
// (keys are sorted alphabetically internally)
// ── Nested object ─────────────────────────────────────────────
json order = {
{"id", "ord-001"},
{"total", 49.99},
{"user", {
{"name", "Bob"},
{"email", "bob@example.com"}
}},
{"tags", {"sale", "priority"}} // nested array
};
// ── JSON array from initializer list ──────────────────────────
json nums = {1, 2, 3, 4, 5};
json mixed = {1, "two", 3.0, true, nullptr};
json empty = json::array(); // explicit empty array
json emptyO = json::object(); // explicit empty object
// ── Build incrementally ───────────────────────────────────────
json items = json::array();
items.push_back({{"id", 1}, {"name", "Widget"}});
items.push_back({{"id", 2}, {"name", "Gadget"}});
items[0]["price"] = 9.99; // add field to existing element
// ── Construct from STL containers ─────────────────────────────
std::vector<int> v = {10, 20, 30};
json jv = v; // [10, 20, 30]
std::map<std::string, int> scores = {{"Alice", 95}, {"Bob", 87}};
json js = scores; // {"Alice":95,"Bob":87}
// ── Serialize: dump() ─────────────────────────────────────────
std::string compact = person.dump(); // one-line, no spaces
std::string pretty = person.dump(4); // 4-space indent
std::string tab = person.dump(1, ' '); // tab indent
std::cout << pretty << "\n";
// Output:
// {
// "active": true,
// "age": 30,
// "alias": null,
// "name": "Alice",
// "score": 98.5
// }
return 0;
}The dump(indent, indent_char) overload controls both the number of spaces and the character used. j.dump(2) is a common choice for configuration files; j.dump() (no indent) produces compact output for network transmission. nlohmann/json stores object keys in insertion order by default (using std::map internally, which sorts them alphabetically). If you need insertion-order preservation, use the nlohmann::ordered_json type alias, available since version 3.9.
Accessing JSON Values Safely
nlohmann/json offers three key access patterns with different error behaviors. Understanding which to use prevents unexpected exceptions or silent null insertions at runtime. The value() method is the most defensive choice for external data; at() is appropriate for validated internal data where a missing key is a programming error.
#include <nlohmann/json.hpp>
#include <optional>
using json = nlohmann::json;
int main() {
json j = {
{"name", "Alice"},
{"age", 30},
{"email", nullptr} // null field
};
// ── operator[] — DANGEROUS on const objects ───────────────────
// On non-const json: silently creates the key with null value if missing
// On const json: throws json::out_of_range if missing
std::string name = j["name"].get<std::string>(); // "Alice"
// json& missing = j["missing"]; // silently creates null key — avoid!
// ── at() — throws json::out_of_range if key missing ──────────
try {
int age = j.at("age").get<int>(); // 30
auto x = j.at("missing").get<int>(); // throws!
} catch (const json::out_of_range& e) {
std::cerr << "Key not found: " << e.what() << "\n";
}
// ── value() — returns default if key absent or wrong type ─────
std::string email = j.value("email", std::string{"n/a"}); // "n/a" (null→default)
std::string missing = j.value("missing", std::string{"none"}); // "none"
int age2 = j.value("age", 0); // 30
// ── contains() — check existence before access ────────────────
if (j.contains("name")) {
std::cout << j["name"].get<std::string>() << "\n";
}
// Equivalent older form: j.find("key") != j.end()
// ── is_null() — check for JSON null before typed get() ────────
if (!j["email"].is_null()) {
std::string em = j["email"].get<std::string>();
}
// ── JSON Pointer access (RFC 6901) ────────────────────────────
json nested = {
{"user", {
{"profile", {
{"name", "Bob"},
{"age", 25}
}}
}}
};
// /user/profile/name traverses nested objects in one call
std::string n = nested.value("/user/profile/name"_json_pointer, std::string{"unknown"});
// "Bob" — or "unknown" if any level is missing
// ── Type checking before get<T>() ─────────────────────────────
json val = 42;
if (val.is_number_integer()) {
int i = val.get<int>();
} else if (val.is_string()) {
std::string s = val.get<std::string>();
}
// ── get_to() — assign directly to a variable ──────────────────
int result;
j.at("age").get_to(result); // result == 30
// ── std::optional support (C++17, nlohmann >= 3.11) ──────────
// json opt_val = j.value("age", std::optional<int>{});
// Requires nlohmann >= 3.11 with std::optional<T> specialization
return 0;
}The JSON Pointer syntax ("/a/b/c"_json_pointer) is the safest way to access deeply nested values with a single default — it avoids a chain of contains() checks for each nesting level. The _json_pointer user-defined literal requires using namespace nlohmann::literals or the full nlohmann::json_pointer<std::string> constructor. For arrays, JSON Pointer uses numeric indices: "/items/0/name"_json_pointer.
Serializing C++ Structs with NLOHMANN_DEFINE_TYPE
The NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE macro generates to_json() and from_json() ADL functions for any struct in a single line. Place it in the same namespace as the struct but outside the struct body. After the macro, the struct participates in all nlohmann/json conversions automatically — assignment, get<T>(), and array deserialization all work without additional code.
#include <nlohmann/json.hpp>
#include <string>
#include <vector>
#include <optional>
using json = nlohmann::json;
// ── Basic struct ──────────────────────────────────────────────────
struct Address {
std::string street;
std::string city;
std::string country;
};
// Non-intrusive: defined outside the struct, in the same namespace
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(Address, street, city, country)
struct User {
int id;
std::string name;
std::string email;
Address address; // nested struct — works automatically
std::vector<std::string> tags; // std::vector serializes as JSON array
};
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(User, id, name, email, address, tags)
// ── Struct with optional fields (C++17) ───────────────────────────
// For optional fields, use NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT
// which fills missing JSON keys with C++ default values
struct Config {
std::string host = "localhost";
int port = 8080;
bool tls = false;
int timeout = 30;
};
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(Config, host, port, tls, timeout)
int main() {
// ── Serialize struct → JSON ───────────────────────────────────
User user{
42,
"Alice",
"alice@example.com",
{"123 Main St", "Anytown", "US"},
{"admin", "user"}
};
json j = user; // calls to_json() automatically
std::cout << j.dump(2) << "\n";
// {
// "address": { "city": "Anytown", "country": "US", "street": "123 Main St" },
// "email": "alice@example.com",
// "id": 42,
// "name": "Alice",
// "tags": ["admin", "user"]
// }
// ── Deserialize JSON → struct ─────────────────────────────────
std::string payload = R"({
"id": 99,
"name": "Bob",
"email": "bob@example.com",
"address": {"street":"456 Oak Ave","city":"Springfield","country":"US"},
"tags": ["user"]
})";
json jp = json::parse(payload);
User parsed = jp.get<User>(); // calls from_json() automatically
std::cout << parsed.name << " in " << parsed.address.city << "\n";
// "Bob in Springfield"
// ── Vector of structs ─────────────────────────────────────────
// json array_payload = json::parse("[{...}, {...}]");
// std::vector<User> users = array_payload.get<std::vector<User>>();
// Deserialization of JSON arrays of structs works automatically
// ── Config with defaults ──────────────────────────────────────
json partial = json::parse(R"({"port":443,"tls":true})");
Config cfg = partial.get<Config>(); // host="localhost", timeout=30 from defaults
std::cout << cfg.host << ":" << cfg.port << " tls=" << cfg.tls << "\n";
// "localhost:443 tls=1"
// ── Intrusive macro (inside the class) ───────────────────────
// Use NLOHMANN_DEFINE_TYPE_INTRUSIVE when you control the class definition
// struct MyClass {
// int x; std::string y;
// NLOHMANN_DEFINE_TYPE_INTRUSIVE(MyClass, x, y)
// };
return 0;
}NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT (available since nlohmann/json 3.11) is preferred for configuration structs where JSON fields may be absent — it uses the C++ default member initializers as fallbacks. Without _WITH_DEFAULT, a missing JSON key throws json::out_of_range during deserialization. For enum fields, define NLOHMANN_JSON_SERIALIZE_ENUM to map enum values to JSON strings — a cleaner alternative to manually casting to int.
Iterating JSON Objects and Arrays
nlohmann/json integrates with C++ range-based for loops and structured bindings (C++17). Objects yield key-value pairs via items(); arrays yield elements directly. The STL-style iterator API means you can use standard algorithms like std::find_if and std::transform on JSON values.
#include <nlohmann/json.hpp>
#include <algorithm>
#include <numeric>
using json = nlohmann::json;
int main() {
json obj = {
{"alice", 95},
{"bob", 87},
{"carol", 92}
};
json arr = {10, 20, 30, 40, 50};
// ── Iterate JSON object — keys and values ─────────────────────
for (auto& [key, value] : obj.items()) {
// key : const std::string&
// value : const json&
std::cout << key << " scored " << value.get<int>() << "\n";
}
// alice scored 95
// bob scored 87
// carol scored 92
// ── Iterate JSON array (range-based for) ──────────────────────
for (const auto& elem : arr) {
std::cout << elem.get<int>() << " ";
}
std::cout << "\n"; // 10 20 30 40 50
// ── Iterate with index ────────────────────────────────────────
for (size_t i = 0; i < arr.size(); ++i) {
std::cout << "[" << i << "] = " << arr[i].get<int>() << "\n";
}
// ── STL algorithms on JSON arrays ─────────────────────────────
// Find first element > 25
auto it = std::find_if(arr.begin(), arr.end(),
[](const json& v) { return v.get<int>() > 25; });
if (it != arr.end()) {
std::cout << "First > 25: " << it->get<int>() << "\n"; // 30
}
// Sum all elements
int total = std::accumulate(arr.begin(), arr.end(), 0,
[](int acc, const json& v) { return acc + v.get<int>(); });
std::cout << "Sum: " << total << "\n"; // 150
// ── Build a new JSON array by transforming an existing one ─────
json doubled = json::array();
std::transform(arr.begin(), arr.end(), std::back_inserter(doubled),
[](const json& v) { return v.get<int>() * 2; });
// doubled == [20, 40, 60, 80, 100]
// ── Filter a JSON array ───────────────────────────────────────
json high_scores = json::array();
for (auto& [name, score] : obj.items()) {
if (score.get<int>() >= 90) {
high_scores.push_back({{"name", name}, {"score", score}});
}
}
// high_scores == [{"name":"alice","score":95},{"name":"carol","score":92}]
std::cout << high_scores.dump(2) << "\n";
// ── Modify values in-place ────────────────────────────────────
for (auto& [key, value] : obj.items()) {
value = value.get<int>() + 5; // add 5 to every score
}
return 0;
}Structured bindings (auto& [key, value]) require C++17 (-std=c++17). In C++11/14, use the iterator form: for (auto it = obj.begin(); it != obj.end(); ++it) { it.key(); it.value(); }. Modifying values in-place during items() iteration is safe — the reference returned by value is a direct reference to the stored JSON element, so assignment writes through to the underlying DOM.
Reading and Writing JSON Files
File I/O with nlohmann/json uses standard C++ <fstream>. The library accepts any std::istream for reading and any std::ostream for writing, making it work with files, string streams, and network stream wrappers equally. Error handling covers both file open failures (checked manually) and parse failures (caught as json::parse_error).
#include <nlohmann/json.hpp>
#include <fstream>
#include <iostream>
#include <stdexcept>
using json = nlohmann::json;
// ── Read a JSON file ──────────────────────────────────────────────
json readJsonFile(const std::string& path) {
std::ifstream file(path);
if (!file.is_open()) {
throw std::runtime_error("Cannot open file: " + path);
}
json j;
try {
file >> j; // stream extraction operator
// equivalent: j = json::parse(file);
} catch (const json::parse_error& e) {
throw std::runtime_error(
"JSON parse error in " + path + " at byte " +
std::to_string(e.byte) + ": " + e.what()
);
}
return j;
}
// ── Write a JSON file ─────────────────────────────────────────────
void writeJsonFile(const std::string& path, const json& j, int indent = 2) {
std::ofstream file(path);
if (!file.is_open()) {
throw std::runtime_error("Cannot write file: " + path);
}
file << j.dump(indent) << "\n";
// file closes automatically (RAII)
}
// ── Atomic write: write to temp file then rename ──────────────────
void writeJsonFileAtomic(const std::string& path, const json& j, int indent = 2) {
std::string tmp = path + ".tmp";
{
std::ofstream file(tmp);
if (!file.is_open()) throw std::runtime_error("Cannot open tmp file");
file << j.dump(indent) << "\n";
}
// rename is atomic on POSIX (Linux/macOS) for same-filesystem moves
if (std::rename(tmp.c_str(), path.c_str()) != 0) {
throw std::runtime_error("Cannot rename file: " + tmp);
}
}
int main() {
// ── Read config.json ──────────────────────────────────────────
try {
json config = readJsonFile("config.json");
std::string host = config.value("host", std::string{"localhost"});
int port = config.value("port", 8080);
std::cout << host << ":" << port << "\n";
} catch (const std::runtime_error& e) {
std::cerr << "Error: " << e.what() << "\n";
}
// ── Write output.json ─────────────────────────────────────────
json result = {
{"status", "ok"},
{"processed", 42},
{"errors", json::array()}
};
writeJsonFile("output.json", result, 4);
// ── Update a JSON file in-place ───────────────────────────────
json data = readJsonFile("data.json");
data["updated_at"] = "2026-05-28T00:00:00Z";
data["version"] = data.value("version", 0) + 1;
writeJsonFileAtomic("data.json", data, 2);
// ── Stream output to stdout ───────────────────────────────────
json j = {{"hello", "world"}, {"count", 42}};
std::cout << j.dump(2) << "\n";
return 0;
}For files larger than ~50 MB, nlohmann/json may use significant memory since it builds the full DOM. In that case, use RapidJSON's FileReadStream with a SaxReader callback, or process the file as NDJSON line-by-line — each line is a valid small JSON object that nlohmann/json can parse efficiently. The atomic write pattern (write to .tmp, rename) prevents corrupted files if the process crashes mid-write.
nlohmann/json vs RapidJSON: When to Use Each
The two dominant C++ JSON libraries occupy different points on the ergonomics-performance spectrum. nlohmann/json wins on developer experience; RapidJSON wins on raw throughput. The 10× speed difference matters for high-frequency parsing paths — game asset loading, financial data feeds, log processing pipelines. For most application code, nlohmann/json's cleaner API is the better tradeoff.
// ─────────────────────────────────────────────────────────────────
// nlohmann/json — ergonomic DOM API
// Best for: config files, REST payloads, < 1 MB, rapid development
// ─────────────────────────────────────────────────────────────────
#include <nlohmann/json.hpp>
using json = nlohmann::json;
void parseWithNlohmann(const std::string& input) {
json j = json::parse(input);
auto name = j.at("name").get<std::string>();
auto age = j.at("age").get<int>();
std::cout << name << " (" << age << ")\n";
}
// Struct round-trip in 2 lines of boilerplate:
struct Person { std::string name; int age; };
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(Person, name, age)
// Usage: Person p = json::parse(input).get<Person>();
// json j = person_instance;
// ─────────────────────────────────────────────────────────────────
// RapidJSON — SAX streaming, ~10× faster for large files
// Best for: > 1 MB payloads, hot paths, memory-constrained systems
// ─────────────────────────────────────────────────────────────────
#include <rapidjson/document.h>
#include <rapidjson/writer.h>
#include <rapidjson/stringbuffer.h>
#include <rapidjson/filereadstream.h>
#include <rapidjson/reader.h>
using namespace rapidjson;
void parseWithRapidDOM(const std::string& input) {
Document doc;
doc.Parse(input.c_str());
if (doc.HasParseError()) {
std::cerr << "RapidJSON error at offset "
<< doc.GetErrorOffset() << "\n";
return;
}
// Access — more verbose than nlohmann, no type safety without checks
if (doc.HasMember("name") && doc["name"].IsString()) {
std::string name = doc["name"].GetString();
}
}
// ── RapidJSON SAX handler — for large files, O(1) memory ─────────
struct MyHandler {
bool String(const char* str, SizeType len, bool /*copy*/) {
std::cout << "String: " << std::string(str, len) << "\n";
return true; // return false to abort parsing
}
bool Int(int i) { std::cout << "Int: " << i << "\n"; return true; }
bool Double(double d) { std::cout << "Dbl: " << d << "\n"; return true; }
bool Bool(bool b) { std::cout << "Bool: " << b << "\n"; return true; }
bool Null() { std::cout << "Null\n"; return true; }
bool StartObject() { return true; }
bool Key(const char* s, SizeType len, bool) {
std::cout << "Key: " << std::string(s, len) << "\n"; return true;
}
bool EndObject(SizeType) { return true; }
bool StartArray() { return true; }
bool EndArray(SizeType) { return true; }
bool Uint(unsigned u) { return Int(static_cast<int>(u)); }
bool Int64(int64_t i) { return Int(static_cast<int>(i)); }
bool Uint64(uint64_t u) { return Int(static_cast<int>(u)); }
bool RawNumber(const char*, SizeType, bool) { return true; }
};
void parseWithRapidSAX(FILE* fp) {
char buf[65536];
FileReadStream stream(fp, buf, sizeof(buf));
MyHandler handler;
Reader reader;
reader.Parse(stream, handler); // O(1) memory — no DOM allocation
}
// ─────────────────────────────────────────────────────────────────
// Decision matrix
// ─────────────────────────────────────────────────────────────────
//
// Criterion nlohmann/json RapidJSON
// ─────────────────────────────────────────────────────────────────
// Parse speed (1 KB) 0.5 MB/s 5 MB/s
// Parse speed (100 MB) 0.2 MB/s 2 MB/s
// Memory (parse) Full DOM SAX: O(1)
// Header-only ✓ ✓
// Struct macros ✓ (1 line) ✗ (manual)
// Initializer construction ✓ ✗
// STL containers ✓ (auto) ✗ (manual)
// JSON Pointer ✓ ✓
// SAX streaming ✗ ✓
// C++ standard required C++11 C++03
// License MIT MIT
//
// Rule of thumb:
// < 1 MB payloads → nlohmann/json
// > 1 MB, or in hot path → RapidJSON SAX
// Both available → nlohmann for correctness, RapidJSON to optimize
}A practical approach in performance-critical codebases: use nlohmann/json during development and testing (faster to write, easier to debug), then profile with real data volumes. If JSON parsing shows up in the top 10% of CPU time, rewrite just those code paths with RapidJSON SAX. The two libraries can coexist in the same project — there is no linkage conflict. Other options worth knowing: simdjson (SIMD-accelerated, 2× faster than RapidJSON for bulk parsing) and glaze (modern C++23 library with compile-time reflection, no macros needed).
Key Terms
- DOM parsing
- Document Object Model parsing loads the entire JSON input into memory as a tree of nodes before returning control to the caller. Both nlohmann/json and RapidJSON's
Document::Parse()use DOM parsing. The resulting tree supports random access — you can navigate to any field in any order. The tradeoff is that DOM parsing uses memory proportional to the input size, making it impractical for files larger than available RAM. nlohmann/json represents each JSON value as annlohmann::jsonobject backed by astd::variant-like discriminated union internally. DOM parsing is the correct default for JSON payloads under ~10 MB. - SAX parsing
- Simple API for XML (adapted to JSON) is an event-driven streaming parser that calls user-defined handler methods as it encounters JSON tokens —
StartObject(),Key(),String(),EndObject(), etc. — without building a tree. RapidJSON'sReader::Parse(stream, handler)implements SAX-style parsing. Because no DOM is allocated, memory usage is O(1) regardless of input size. The tradeoff is that SAX parsers require more boilerplate: you must maintain your own state machine to reconstruct structured data from the token stream. SAX is the right choice for NDJSON log processing, streaming API responses, and files too large to fit in memory. - ADL (Argument-Dependent Lookup)
- nlohmann/json uses C++ Argument-Dependent Lookup to call user-defined
to_json(nlohmann::json&, const T&)andfrom_json(const nlohmann::json&, T&)free functions. When you writejson j = my_struct, the compiler looks upto_jsonin the namespaces associated withjsonand the struct type. TheNLOHMANN_DEFINE_TYPE_NON_INTRUSIVEmacro generates these functions in the same namespace as the struct, making ADL find them automatically. This design means you never need to inherit from a base class or register a type — adding the macro in the right namespace is all that is required for full nlohmann/json integration. - JSON Pointer
- JSON Pointer (RFC 6901) is a string syntax for identifying a specific value within a JSON document using a slash-separated path.
"/user/address/city"navigates from the root object to theuserfield, thenaddress, thencity. Array elements use numeric indices:"/items/0". In nlohmann/json, JSON Pointers are used withj.at(json::json_pointer("/user/city")), the"path"_json_pointerUDL, andj.value("/path"_json_pointer, default_value)for default-safe deep access. The empty string""refers to the entire document. Forward slashes in key names are escaped as~1; tildes as~0. - header-only library
- A header-only C++ library distributes all its code in
.hppheader files — there is no separate.lib,.a, or.soto compile and link against. Including the header is all that is required. nlohmann/json's entire implementation fits in a singlejson.hppfile (~23,000 lines). The advantage is trivial integration: copy the file into your project or add a CMake FetchContent block. The disadvantage is slower compile times — every translation unit that includes the header recompiles the full library. CMake'starget_precompile_headersor a forward-declaration header (nlohmann/json_fwd.hpp) mitigates this cost in large projects.
FAQ
How do I parse a JSON string in C++?
Use nlohmann/json with a single #include <nlohmann/json.hpp>. Call json::parse(str) where str is a std::string or string literal containing valid JSON. The function returns an nlohmann::json object. For example: auto j = json::parse("{\"name\":\"Alice\",\"age\":30}"); — j is now a JSON object you can query. If the input is invalid JSON, json::parse throws a json::parse_error exception. To avoid throwing, pass false as the third argument: auto j = json::parse(str, nullptr, false); which returns a discarded value on failure. Check with j.is_discarded(). The library also accepts C++ streams directly: json j = json::parse(input_stream); reading from std::cin, std::ifstream, or any std::istream.
How do I serialize a C++ struct to JSON?
nlohmann/json provides the NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE macro for automatic struct serialization. Place it outside the struct in the same namespace: NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(MyStruct, field1, field2, field3). This generates to_json() and from_json() functions the library calls automatically. After the macro, json j = my_struct_instance; serializes to JSON, and MyStruct s = j.get<MyStruct>(); deserializes back. The macro works with all JSON-compatible field types: std::string, int, double, bool, std::vector<T>, std::map<std::string,T>, and nested structs that also have the macro defined. For structs where fields may be absent from the JSON, use NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT — it uses C++ default member initializers as fallbacks, eliminating json::out_of_range throws for optional fields. This represents roughly 20× less boilerplate than manual from_json/to_json implementations.
How do I safely access a JSON field in C++ without throwing?
Use j.value("key", default_value) to access a JSON field with a fallback when the key is missing or the type does not match. For example: std::string name = j.value("name", std::string{"unknown"}); returns "unknown" if "name" is absent or not a string. Contrast with j["key"] which silently creates a null entry for missing keys on non-const json objects, and j.at("key") which throws json::out_of_range if the key is absent. Use j.contains("key") to check key existence before accessing: if (j.contains("name")) { auto name = j["name"].get<std::string>(); }. For null-safe access use j.at("key").is_null() before calling .get<T>(). The value() overload also supports nested access with JSON Pointer syntax: j.value("/user/name"_json_pointer, std::string{"default"}) — a pattern that eliminates 3–5 lines of nested contains() checks for deeply nested fields.
What is the difference between nlohmann/json and RapidJSON?
nlohmann/json and RapidJSON solve different problems. nlohmann/json prioritizes developer ergonomics: a single header file, STL-style iteration, initializer list construction, and zero-boilerplate struct serialization macros. It is the most downloaded C++ JSON library on package managers. RapidJSON prioritizes raw throughput: it is approximately 10× faster than nlohmann/json for large files using SAX-style event-driven streaming parsing that avoids allocating a full DOM tree. RapidJSON benchmarks at roughly 2 MB/s vs nlohmann at 0.2 MB/s for large payloads. Choose nlohmann/json for configuration files, API payloads under 1 MB, and any code where development speed matters more than parse speed. Choose RapidJSON for high-frequency trading systems, game engine asset loading, log ingestion pipelines, or any path where JSON parsing appears in a CPU profile. Both are header-only and MIT-licensed. A third option, simdjson, uses SIMD CPU instructions and is 2× faster than RapidJSON for bulk parsing of large files.
How do I iterate over a JSON object in C++?
nlohmann/json supports range-based for loops and structured bindings (C++17). To iterate over a JSON object with key-value access, use json::items(): for (auto& [key, value] : j.items()) { std::cout << key << ": " << value << "\n"; }. The key is a const std::string reference; value is a const nlohmann::json reference. To iterate over a JSON array: for (auto& element : j) {'{ ... }'} — the range loop directly yields each JSON element. For typed iteration, call .get<T>() on the element: for (auto& el : j) { auto n = el.get<int>(); }. To iterate with an index: for (size_t i = 0; i < j.size(); ++i) {'{ auto& el = j[i]; }'}. Both object and array iteration are O(n). Calling j.size() returns the number of keys for objects and the number of elements for arrays.
How do I read a JSON file in C++ with nlohmann/json?
Open a std::ifstream and pass it to json::parse() or use the stream insertion operator. The recommended pattern is: #include <fstream> followed by std::ifstream file("config.json"); if (!file.is_open()) {'{ throw std::runtime_error("cannot open"); }'} json j = json::parse(file);. Alternatively use the << operator: json j; file >> j; — both forms are equivalent. For large files where memory is a concern (files over 50 MB), consider using RapidJSON with its FileReadStream and SAX reader instead, which processes the file event-by-event without building a full DOM. To write a JSON file: std::ofstream out("output.json"); out << j.dump(4) << "\n"; — the integer argument to dump() specifies the indentation width; 4 produces standard 4-space indented output.
How do I handle JSON parse errors in C++?
nlohmann/json throws json::parse_error (a subclass of std::exception) when the input is not valid JSON. Catch it with: try {'{'} auto j = json::parse(str); {'}'} catch (const json::parse_error& e) {'{'} std::cerr << "Parse error at byte " << e.byte << ": " << e.what() << "\n"; {'}'}. The e.byte field gives the 1-based byte offset where the error occurred. For non-throwing parse, call json::parse(str, nullptr, false) which returns a discarded value: auto j = json::parse(str, nullptr, false); if (j.is_discarded()) {'{'} /* handle error */ {'}'}. Beyond parse errors, catch json::type_error for type mismatch (accessing a string field as an integer) and json::out_of_range for missing key access via j.at("key"). All nlohmann/json exceptions derive from json::exception, so catch (const json::exception& e) with e.id providing a numeric error code catches all library errors.
How do I create a JSON array in C++?
nlohmann/json constructs JSON arrays from C++ initializer lists. An array literal uses a list without key-value pairs: json arr = {"{"}1, 2, 3, "four", true, nullptr{"}"}; — the library infers the JSON array type from the mixed-type initializer. To create an empty array and push values: json arr = json::array(); arr.push_back(42); arr.push_back("hello");. To create a typed array from a std::vector: std::vector<int> v = {"{"}10, 20, 30{"}"}; json j = v; — the implicit conversion handles std::vector, std::list, std::deque, std::set, and std::array automatically. Array access is 0-based with j[0], j[1], etc. j.size() returns the element count. j.at(i) throws json::out_of_range for out-of-bounds index. To serialize back to a string: std::string s = arr.dump(); returns "[1,2,3,\"four\",true,null]".
Validate your JSON payloads instantly
Paste any JSON into Jsonic's validator to check syntax, format, and schema compliance — no C++ compiler required.
Open JSON ValidatorFurther reading and primary sources
- nlohmann/json GitHub Repository — Source, docs, examples, and release notes for the most popular C++ JSON library
- nlohmann/json Documentation — Full API reference: parse, dump, structured bindings, macros, iterators, and type conversions
- RapidJSON Documentation — RapidJSON guide covering DOM and SAX APIs, schema validation, and performance tuning
- RapidJSON GitHub Repository — Source and benchmarks for RapidJSON — the fastest C++ JSON library for large files
- simdjson: Parsing gigabytes of JSON per second — SIMD-accelerated C++ JSON parser — 2× faster than RapidJSON using AVX2/SSE4.2 instructions