nlohmann/json: JSON for Modern C++
What nlohmann/json is
nlohmann/json, published under the name "JSON for Modern C++", is a JSON library for C++ written by Niels Lohmann. Its pitch is not speed but ergonomics: the README's first design goal is "intuitive syntax", with the note that in languages such as Python "JSON feels like a first-class data type", and that the library uses "all the operator magic of modern C++ to achieve the same feeling in your code". In practice that means you build a JSON object with initializer lists, read it with j["key"], iterate it like an STL container and convert your own structs with two functions or a one-line macro.
The second design goal explains a lot of its popularity: "trivial integration". The whole library is a single header, json.hpp, written in "vanilla C++11", with "no library, no subproject, no dependencies, no complex build system". The third goal is "serious testing": the README states 100% code coverage including exceptional behaviour, checks with Valgrind and the Clang sanitizers, and continuous fuzzing of all parsers through Google OSS-Fuzz.
The project is just as clear about what it does not optimise for. Memory efficiency and speed are listed under "other aspects" that "were not so important", with the line that has stayed in the README for years: "There are certainly faster JSON libraries out there. However, if your goal is to speed up your development by adding JSON support with a single header, then this library is the way to go." The audience is therefore application and tooling code in C++ that reads configuration files, talks to HTTP APIs, writes reports or exchanges messages, where developer time matters more than the last gigabyte per second of parsing throughput.
The repository was created on 4 July 2013, is MIT-licensed, and with roughly 50,900 stars it is one of the most widely starred C++ libraries on GitHub. Documentation lives at json.nlohmann.me.
How it works
One class template, one value type
Everything revolves around the class template basic_json. The familiar nlohmann::json is an alias for it with default template arguments, and the README spells out what those defaults are: std::string for strings, int64_t, uint64_t or double for numbers, std::map for objects, std::vector for arrays and bool for Booleans. The architecture page describes each value as a tagged union, an enumeration value_t naming the type (null, object, array, string, boolean, three number kinds, binary and "discarded") next to a union holding the content. The performance page puts a value at 16 bytes on a 64-bit platform, with strings, arrays, objects and binary values allocated on the heap and owned by the value.
Because the container types are template parameters, they can be swapped. nlohmann::ordered_json is the built-in example: it replaces std::map with nlohmann::ordered_map so object keys keep their insertion order instead of being sorted. The object-order page also describes adapters for hash maps when order does not matter and only lookup speed does.
Conversions through ADL
User types are converted through a serializer that defaults to nlohmann::adl_serializer. It calls free functions to_json and from_json found by argument-dependent lookup in your type's namespace. That is why the README insists those functions "MUST be in your type's namespace" and visible wherever the conversion is used. For third-party types you cannot touch, such as boost::optional, you specialise adl_serializer in the nlohmann namespace instead.
Parsing is SAX underneath
The parser emits SAX events internally. parse() feeds them to a consumer that builds the tree (the DOM), accept() only checks validity, and sax_parse() hands the events to your own handler, which is the documented route for documents too large to hold as a tree. The README is explicit that both the DOM parser and the acceptor are built on that SAX interface. Inputs can be strings, streams, FILE*, iterator pairs, contiguous containers or a custom iterator that models LegacyInputIterator.
Key features
The list below sticks to what is in the latest release, 3.12.0. Features that the README on the develop branch already describes but that are not in the 3.12.0 header are covered in their own section further down.
- JSON as a native-feeling type. Build values with
j["pi"] = 3.141, nested initializer lists,json::array()andjson::object(), or parse text with the_jsonuser-defined literal. - STL container behaviour. The README says the class satisfies the ReversibleContainer requirement:
push_back,emplace_back, iterators, range-based for,items()with structured bindings,find,count,containsanderaseall work as expected. - Conversion from STL containers. Any sequence container becomes a JSON array and any associative key-value container a JSON object, as long as the elements are convertible.
- Arbitrary type conversion. Write
to_json/from_jsonby hand or use theNLOHMANN_DEFINE_TYPE_*macro family: intrusive (inside the class, for private members) or non-intrusive (in the namespace), with_WITH_DEFAULTto tolerate missing keys and_ONLY_SERIALIZEfor one-way conversion. 3.12.0 addedNLOHMANN_DEFINE_DERIVED_TYPEfor derived classes and made the macros work withordered_json. The docs limit the macros to 63 member variables. - Enum mapping.
NLOHMANN_JSON_SERIALIZE_ENUMmaps enum values to strings instead of the default integers, with the first pair acting as the fallback for unknown values. - JSON Pointer, Patch and Merge Patch. RFC 6901 pointers (
"/baz/1"_json_pointer),flatten()/unflatten(), RFC 6902patch()anddiff(), and RFC 7396merge_patch(). - Binary formats. In 3.12.0: BSON, CBOR, MessagePack, UBJSON and BJData, each with
to_*andfrom_*functions, plus a binary value type for byte strings from BSON, CBOR and MessagePack. - Relaxed parsing on request. Comments are rejected by default, as the JSON specification requires; passing
ignore_commentstoparsetreats them as whitespace. - Error handling choices. Typed exceptions with numbered ids (
parse_error.101,type_error.304,out_of_range.403and so on), aparse(input, nullptr, false)form that returns a discarded value instead of throwing,accept(), or building without exceptions (-fno-exceptionsorJSON_NOEXCEPTION), in which case errors callabort(). - Diagnostics.
JSON_DIAGNOSTICSadds a JSON Pointer to the offending value in exception messages, andJSON_DIAGNOSTIC_POSITIONS(new in 3.12.0) records byte offsets exposed throughstart_pos()andend_pos(). Both cost memory per value and are off by default. - Standards behaviour. The README says the library "targets strict conformance with RFC 8259", runs JSONTestSuite in CI, accepts only UTF-8 (plus
std::u16string/std::u32stringinput), and rejects invalid surrogates.
Getting started
For a project without a package manager, the integration docs say the library "is one file": download json.hpp from the releases page, put it in a directory named nlohmann on the include path and include it. The README's snippet:
#include <nlohmann/json.hpp>
// for convenience
using nlohmann::json;
Then compile with C++11 or later enabled, for example -std=c++11 for GCC and Clang. With CMake and an installed copy, the README uses find_package and the namespaced interface target:
find_package(nlohmann_json 3.12.0 REQUIRED)
add_library(foo ...)
target_link_libraries(foo PRIVATE nlohmann_json::nlohmann_json)
To download a release at configure time (CMake 3.14 or later), the recommended form is the release tarball URL rather than a Git clone, which the CMake docs warn is "quite large":
include(FetchContent)
FetchContent_Declare(json URL https://github.com/nlohmann/json/releases/download/v3.12.0/json.tar.xz)
FetchContent_MakeAvailable(json)
target_link_libraries(foo PRIVATE nlohmann_json::nlohmann_json)
The package-manager page covers more than a dozen ecosystems. A few of the documented commands:
brew install nlohmann-json
vcpkg install nlohmann-json
meson wrap install nlohmann_json
pkg-config nlohmann_json --cflags
Conan users add nlohmann_json/x.y.z to the requires of their conanfile. The repository also ships a MODULE.bazel and BUILD.bazel, so Bazel projects depend on @nlohmann_json//:json, with versions available through the Bazel Central Registry. Swift Package Manager, NuGet, Conda, MSYS2, MacPorts, build2, CPM.cmake, Spack, Hunter, cget and xmake are listed too.
Once the header is in place, a user type needs one line in its namespace, straight from the README:
namespace ns {
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(person, name, address, age)
}
Hands-on: compiling the README examples against 3.12.0
On 15 November 2026 the release-tagged single header (v3.12.0/single_include/nlohmann/json.hpp from raw.githubusercontent.com) was downloaded onto a 2-vCPU Linux x86_64 cloud VM running Ubuntu 24.04 with g++ 13.3.0. One test program combined the README's examples: the _json literal, ordered_json, the binary-format round trip, JSON Pointer and Patch, Merge Patch, the person struct with NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE, and deliberately broken input. Everything compiled and ran on the first attempt. Output, lightly trimmed:
| Check | Real output |
|---|---|
| Version macros | version 3.12.0 |
"{ \"happy\": true, \"pi\": 3.141 }"_json then dump() | {"happy":true,"pi":3.141}; equal to the same text passed to json::parse: true |
| Keys one, two, three inserted in that order | json: {"one":1,"three":3,"two":2}; ordered_json: {"one":1,"two":2,"three":3} |
{"compact":true,"schema":0} (27 bytes as JSON text) | CBOR 18 bytes a2 67 63 6f 6d 70 61 63 74 f5 66 73 63 68 65 6d 61 00; MessagePack 18 bytes; UBJSON 22 bytes; BSON 27 bytes. All four byte sequences match the README's comments exactly, and all four round trips compared equal. |
1,000 records of {id, name, price, active} | JSON 56,893 bytes; CBOR 38,613 (67.9%); MessagePack 38,509 (67.7%); UBJSON 49,636 (87.2%) |
j_original["/baz/1"_json_pointer] | "two" |
README JSON Patch, then json::diff | {"baz":"boo","hello":["world"]}; diff: [{"op":"replace","path":"/baz",...},{"op":"remove","path":"/hello"},{"op":"add","path":"/foo","value":"bar"}] |
flatten() | {"/baz/0":"one","/baz/1":"two","/baz/2":"three","/foo":"bar"} |
README merge_patch | {"a":"z","c":{"d":"e"}} |
json j = p; for the person struct | {"address":"744 Evergreen Terrace","age":60,"name":"Ned Flanders"}, and back via get<ns::person>() |
get<ns::person>() on an object without address | [json.exception.out_of_range.403] key 'address' not found |
json::parse(R"({"a": 1,})") (trailing comma) | [json.exception.parse_error.101] parse error at line 1, column 9: syntax error while parsing object key - unexpected '}'; expected string literal, byte 9 |
json::parse("[1, 2") | ... syntax error while parsing array - unexpected end of input; expected ']' |
at("x") on a number; at("missing") on an object | [json.exception.type_error.304] cannot use at() with number; [json.exception.out_of_range.403] key 'missing' not found |
parse("{oops}", nullptr, false); accept | is_discarded() true; accept("[1,2,3]") true, accept("[1,2,") false |
The cost side was measured the same way. Times are the best of three runs of g++ -std=c++17, so they show orders of magnitude, not a benchmark:
| Measurement | v3.12.0 header | develop header (pre-3.13.0) |
|---|---|---|
| Single header size | 953,436 bytes, 25,526 lines | 1,929,376 bytes, 48,215 lines |
| Hello-world without the library | 0.22 s | 0.22 s |
One-line parse-and-dump program, -O0 / -O2 | 1.63 s / 2.60 s | 2.82 s / 4.53 s |
Full test program above, -O0 / -O2 | 3.64 s / 7.98 s | 5.56 s / 12.97 s (single run) |
Test program binary, -O2 | 485,120 bytes (375,128 stripped) | not measured |
Two practical notes came out of the run. First, the same one-line program also built with -std=c++11 (1.50 s), so the C++11 baseline is real. Second, g++ warned "ignoring return value" on the two json::parse(...) calls whose result was discarded inside a try block: parse carries a warn-unused-result attribute in 3.12.0, so validation-only code should call accept() or keep the result. The compile-time numbers also make the docs' advice concrete: every translation unit that includes json.hpp pays one to several seconds, which is why the FAQ recommends json_fwd.hpp in headers, precompiled headers or C++20 modules.
Release 3.12.0 versus the develop branch
Read the README with care: it is rendered from the develop branch, and the releases page of the docs warns that the documentation "may describe changes that are not part of a release yet". A text search of the two single headers on 15 November 2026 found none of the following in 3.12.0, while each appears in the develop header:
merge_diff, which computes a merge patch from two values;- the
ignore_trailing_commasparse option (the hands-on run confirms 3.12.0 rejects a trailing comma withparse_error.101); - BON8 (
to_bon8/from_bon8), the sixth binary format; - the
*_WITH_NAMESconversion macros for custom JSON key names andNLOHMANN_JSON_SERIALIZE_ENUM_STRICT, which throws on unknown enum values; JSON_BRACE_INIT_COPY_SEMANTICS, which the FAQ marks "since version 3.13.0";- the zero-copy
json_document/json_viewtypes in a separate<nlohmann/json_view.hpp>header, which the performance page says was "added in version 3.13.0" and which builds a flat index of 16 bytes per value instead of a tree.
The develop docs also contain a long post titled "How nlohmann/json 3.13.0 got 3× faster", dated 28 October 2026. It claims the library's own suite of 182 benchmarks runs 3.3× as fast on an Apple M1 Max (geometric mean) after swapping the 3.12.0 header for the 3.13.0 one, with parsing typical documents 3.5 to 5.1× as fast and writing 2.9 to 6.6×. It names the price as compile time, with two small example programs taking 36 to 68% longer to compile. An editorial comment in the post's source says every number is to be re-measured against the v3.13.0 tag before publishing, and no v3.13.0 tag existed on 15 November 2026, so treat these as pre-release claims. The hands-on compile times above point the same direction: the develop header is about twice the size and compiled roughly 50 to 75% slower in these tests. The "Unreleased" page lists two behaviour changes worth knowing in advance: converting an rvalue container now moves its elements, and in C++20 a std::u8string becomes a JSON string rather than an array of code units.
Use cases
- Configuration files. Parse a file with
json::parse(f), read values with defaults throughvalue(), and map sections onto structs with the conversion macros.ignore_commentshelps with hand-edited files. - REST and RPC clients. Build request bodies with initializer lists and turn responses into typed structs, with
out_of_range.403telling you which key was missing. - Compact messaging. When two programs exchange data and no human reads it,
to_cbororto_msgpackshrink the payload with one call. In the run above that was about two thirds of the JSON size for a record array. The docs' size table shows the gain varies by document shape: 50.5% for number-heavycanada.json, but only about 88% for string-heavyjeopardy.json. - Diffing and patching documents.
diff()pluspatch()give RFC 6902 change sets for audit logs or sync protocols;merge_patch()fits HTTP PATCH endpoints. - Tooling and test fixtures. Golden-file tests, report generators and command-line utilities benefit from the single-header integration more than from raw throughput. Large C/C++ codebases such as llama.cpp or whisper.cpp show the kind of environment where adding one header beats adding a build dependency.
How it compares
The README does not compare itself feature by feature with other libraries. It links to Milo Yip's nativejson-benchmark for "faster JSON libraries", and the develop-branch 3.13.0 post sorts libraries by how they hold parsed data: a mutable tree (nlohmann/json's json, Boost.JSON, RapidJSON and others), a flat document (simdjson's DOM, yyjson, and the upcoming json_document), forward-only reading (SAX in nlohmann/json, RapidJSON's Reader, simdjson On-Demand) and parsing straight into your own types. The table below stays at that level and adds two tools from other ecosystems that cover neighbouring jobs. The rows outside C++ are not alternatives so much as answers to "where should validation happen?".
| Tool | Approach | How it relates to nlohmann/json |
|---|---|---|
| nlohmann/json 3.12.0 | Single-header C++11 library; mutable tree with STL-like API, SAX parsing, conversion macros, five binary formats in this release | Optimised for ease of use and integration; the README itself says faster libraries exist |
| RapidJSON, simdjson, Boost.JSON | C++ JSON libraries that the project's own develop-branch post benchmarks and borrows techniques from | Usually picked when parsing throughput or allocation control matters most; per that post, simdjson's DOM is a flat document and Boost.JSON can build its tree in an arena allocator. No pages here, and a fair comparison depends on the workload |
| Pydantic | Python data validation through type-annotated models | Covers the "JSON into typed objects with validation" job on the Python side; nlohmann/json's from_json checks presence and type of keys but has no declarative validation rules |
| esc-json-form-core | JSON Schema-driven forms | Works at the schema level, which nlohmann/json does not cover; its FAQ points C++ users to the separate json-schema-validator project for JSON Schema |
| DuckDB | Embedded analytical database | A different category: if the job is querying large JSON datasets rather than handling JSON inside a C++ program, a database is often the better tool |
A rough rule: if the JSON handling is a small part of a larger C++ program and the documents are kilobytes to a few megabytes, nlohmann/json's convenience usually wins. If JSON parsing is the hot path, measure a speed-oriented library first, or watch for 3.13.0 and its json_document.
When not to use it: limits and trade-offs
- Throughput is not the goal. The design-goals page says so in as many words, and the performance page suggests "a faster, more specialized library" when "a hard real-time budget or the last percent of throughput" matters more than convenience. The tree representation allocates per string, array and object.
- Compile time is a real cost. Each translation unit that includes the header added roughly 1.3 to 8 seconds of compile time in the hands-on test, depending on program size and optimisation level, and the develop header is twice as large. Plan on
json_fwd.hpp, precompiled headers or modules in big codebases. The modules page calls the C++20 module "experimental". - Keys are sorted by default.
jsonusesstd::map, so output order is alphabetical.ordered_jsonpreserves order but has no lookup index: the docs measure parsing a 16,000-key object at 3.3 ms withjsonversus 181.6 ms withordered_json. Also callordered_json::parse, notjson::parse, or the order is lost. - Brace-initialisation surprises. The FAQ lists it under known bugs:
json j{true};andjson j(true);differ ([true]versustrue), and behaviour differs between GCC and Clang. The opt-in fix is only in the unreleased 3.13.0. - Unchecked access is undefined behaviour.
operator[]on a const object with a missing key is undefined, and on a non-const object it inserts a null. Useat(),value()orcontains()for untrusted data. - UTF-8 only. Latin-1 and other encodings "will yield parse or serialization errors", and
dump()may throw on invalid UTF-8 unless an error handler (replace,ignore,keep) is set. - No schema validation built in. The FAQ says "not directly" and recommends the companion json-schema-validator project.
- Not thread-safe for writes. Concurrent reads are fine; any concurrent write needs external synchronisation, as with
std::map. - Exceptions by default. Without exceptions, errors become
abort()calls, and the docs note that MSVC loses thewhat()text in that mode. Embedded and game code that bans exceptions has to use the non-throwingparseoverload,accept()or SAX. - Licence fine print. The library is MIT, but the README lists bundled third-party code: Hedley (CC0-1.0), parts of Google Abseil (Apache-2.0), and MIT-licensed code from Grisu2, fast_float and Bjoern Hoehrmann's UTF-8 decoder. In the 3.12.0 header the Abseil-derived section is marked as Apache-2.0 code. For most users that changes nothing, but licence scanners will flag it and attribution notices should include it.
Supported compilers and tooling
The develop-branch README lists GCC 4.8 to 16.2, Clang 3.4 to 22.1, Apple Clang 15.0 to 21.0, Intel icpc 2021.10 and icpx 2025.3, NVIDIA nvcc 11.8 to 12.6 and nvc++ 25.5, and MSVC from Visual Studio 2015 (19.0) to Visual Studio 2026 (19.51), each "and possibly later". Unsupported GCC and Clang versions are rejected with #error unless JSON_SKIP_UNSUPPORTED_COMPILER_CHECK is defined. The code also builds with -fno-rtti. CMake options mirror the macros (JSON_ImplicitConversions, JSON_Diagnostics, JSON_MultipleHeaders and others), and Meson exposes the same set without the JSON_ prefix. The README also recommends defining JSON_USE_IMPLICIT_CONVERSIONS to 0 to switch off implicit conversions from JSON values, which it marks "NOT RECOMMENDED" in its examples.
Project activity
Checked 15 November 2026: latest release v3.12.0 (11 April 2025, per the project's releases page), MIT licence, about 50,900 stars on the nlohmann/json repository, last push 15 November 2026. The 3.12.0 header's version macros read 3.12.0, the develop branch still carries the same macros, and 3.13.0 is in preparation but untagged. Before 3.12.0, the 3.11.x line ran from August 2022 to 3.11.3 in November 2023. The repository dates from 4 July 2013, is written in C++, and develops on the develop branch. Questions go to GitHub Discussions, bugs to GitHub issues, and the full documentation, API reference and FAQ are at json.nlohmann.me.
Enjoying this project?
Discover more amazing open-source projects on TechLogHub. We curate the best developer tools and projects.
Repository:https://github.com/nlohmann/json
GitHub - nlohmann/json: nlohmann/json: JSON for Modern C++
nlohmann/json (JSON for Modern C++) is an MIT-licensed single-header C++11 JSON library with STL-like access, struct conversion macros, JSON Pointer, Patch and ...
github - nlohmann/json
Related Projects
Maintain nlohmann/json: JSON for Modern C++? Add the badge to your README
Show that the project is featured on TechLogHub and send readers to this write-up.
[](https://techloghub.com/open-source/nlohmann-json?utm_source=techloghub_badge&utm_medium=referral&utm_campaign=readme)