Docs: fill documentation gaps found in user questions (#5907)

* Docs: fill documentation gaps found in user questions

Add pages on class-hierarchy/smart-pointer/variant/chrono recipes, working
without exceptions, and large, streamed and partial input. Extend the FAQ
(23 entries, Known bugs first), parse error documentation (empty-input
checklist, line/column, diagnostic positions), element access, conversions,
creating values, ADL troubleshooting, memory, and build/distribution notes.
Add 14 examples with generated output. Correct the statement that parse errors
cannot be recovered from with -fno-exceptions.

The topics come from recurring questions on Stack Overflow, Reddit, Hacker
News, YouTube, and the project's GitHub discussions and issues.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* docs: declare range-for lists in variables to keep astyle quiet

astyle reformats a braced init list placed directly in a range-for
header; declare the list first so check_amalgamation passes.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
This commit is contained in:
Niels Lohmann authored and GitHub committed 2026-10-25 12:07:13 +01:00
1 parent cc917d7123
commit ffd4242dc2
45 files changed
+1670 -8

No files matched your search

@@ -0,0 +1,43 @@
#include <iostream>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
int main()
{
json j = json::parse(R"({"name": "Mary", "age": 42})");
// look up the key, then test the type: neither step throws
const auto keys = {"name", "age", "city"};
for (const char* key : keys)
{
const auto it = j.find(key);
if (it == j.end())
{
std::cout << key << ": missing\n";
}
else if (it->is_string())
{
std::cout << key << ": string \"" << it->get_ref<const std::string&>() << "\"\n";
}
else
{
std::cout << key << ": not a string, but " << it->type_name() << '\n';
}
}
// get_ptr returns a null pointer if the value has another type
const std::string* name = j["name"].get_ptr<const std::string*>();
const std::string* age = j["age"].get_ptr<const std::string*>();
std::cout << "get_ptr<string*>: name = " << (name != nullptr) << ", age = " << (age != nullptr) << '\n';
// value() is NOT exception-free: it throws if the key exists with a different type
try
{
std::cout << j.value("name", 0) << '\n';
}
catch (const json::type_error& e)
{
std::cout << e.what() << '\n';
}
}
@@ -0,0 +1,5 @@
name: string "Mary"
age: not a string, but number
city: missing
get_ptr<string*>: name = 1, age = 0
[json.exception.type_error.302] type must be number, but is string
@@ -0,0 +1,48 @@
#include <iostream>
#include <optional>
#include <string>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
struct person
{
std::string name;
int age;
};
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(person, name, age)
// Compile this function with exceptions enabled and call it from code that does not use them.
std::optional<person> to_person(const json& j)
{
try
{
return j.get<person>();
}
catch (const json::exception&)
{
return std::nullopt;
}
}
int main()
{
const json good = {{"name", "Mary"}, {"age", 42}};
const json bad = {{"name", "Mary"}, {"age", "forty-two"}};
const json incomplete = {{"name", "Mary"}};
const auto inputs = {good, bad, incomplete};
for (const json& j : inputs)
{
if (const auto p = to_person(j))
{
std::cout << "person: " << p->name << ", " << p->age << '\n';
}
else
{
std::cout << "not a person: " << j << '\n';
}
}
}
@@ -0,0 +1,3 @@
person: Mary, 42
not a person: {"age":"forty-two","name":"Mary"}
not a person: {"name":"Mary"}
@@ -0,0 +1,28 @@
#include <cstdint>
#include <iostream>
#include <vector>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
// None of the calls below throws, so this example also works with -fno-exceptions.
int main()
{
const std::string invalid = R"({"name": "Mary", "age": )";
// parse() with allow_exceptions = false returns a discarded value
json j = json::parse(invalid, nullptr, false);
std::cout << std::boolalpha << "parse: discarded = " << j.is_discarded() << '\n';
// accept() only tells whether the input is valid JSON
std::cout << "accept: " << json::accept(invalid) << '\n';
// the binary formats have the same parameter
const std::vector<std::uint8_t> truncated_cbor = {0x83, 0x01};
json c = json::from_cbor(truncated_cbor, true, false);
std::cout << "from_cbor: discarded = " << c.is_discarded() << '\n';
// valid input is parsed as usual
json valid = json::parse(R"({"name": "Mary"})", nullptr, false);
std::cout << "valid input: " << valid << '\n';
}
@@ -0,0 +1,4 @@
parse: discarded = true
accept: false
from_cbor: discarded = true
valid input: {"name":"Mary"}
@@ -0,0 +1,42 @@
#include <iostream>
#include <utility>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
// line and column (both starting at 1) of a byte index in a text
std::pair<std::size_t, std::size_t> line_column(const std::string& input, std::size_t byte)
{
std::size_t line = 1;
std::size_t column = 1;
for (std::size_t i = 0; i + 1 < byte && i < input.size(); ++i)
{
if (input[i] == '\n')
{
++line;
column = 1;
}
else
{
++column;
}
}
return {line, column};
}
int main()
{
const std::string input = "{\n \"a\": 1,\n \"b\": ]\n}";
try
{
const json j = json::parse(input);
std::cout << j << '\n';
}
catch (const json::parse_error& e)
{
const auto position = line_column(input, e.byte);
std::cout << e.what() << '\n';
std::cout << "byte " << e.byte << " = line " << position.first << ", column " << position.second << '\n';
}
}
@@ -0,0 +1,2 @@
[json.exception.parse_error.101] parse error at line 3, column 8: syntax error while parsing value - unexpected ']'; expected '[', '{', or a literal
byte 20 = line 3, column 8
@@ -0,0 +1,46 @@
#define JSON_DIAGNOSTIC_POSITIONS 1
#include <iostream>
#include <utility>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
// line and column (both starting at 1) of a byte index in a text
std::pair<std::size_t, std::size_t> line_column(const std::string& input, std::size_t byte)
{
std::size_t line = 1;
std::size_t column = 1;
for (std::size_t i = 0; i + 1 < byte && i < input.size(); ++i)
{
if (input[i] == '\n')
{
++line;
column = 1;
}
else
{
++column;
}
}
return {line, column};
}
int main()
{
const std::string input = "{\n \"name\": \"Mary\",\n \"age\": \"forty-two\"\n}";
const json j = json::parse(input); // the input is valid JSON ...
try
{
const int age = j["age"]; // ... but the value has the wrong type
std::cout << age << '\n';
}
catch (const json::exception& e)
{
std::cout << e.what() << '\n';
// start_pos() is a 0-based index into the input; the helper expects a 1-based byte index
const auto position = line_column(input, j["age"].start_pos() + 1);
std::cout << "the value starts at line " << position.first << ", column " << position.second << '\n';
}
}
@@ -0,0 +1,2 @@
[json.exception.type_error.302] (bytes 29-40) type must be number, but is string
the value starts at line 3, column 10
@@ -0,0 +1,47 @@
#include <iostream>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
std::string buffer;
// call for every chunk; returns true and sets `value` once the buffer holds one complete JSON value
bool push(const std::string& chunk, json& value)
{
buffer += chunk;
try
{
value = json::parse(buffer);
buffer.clear();
return true;
}
catch (const json::parse_error& e)
{
if (e.byte == buffer.size() + 1)
{
return false; // the input ends prematurely: wait for more data
}
throw; // a real syntax error
}
}
int main()
{
json value;
const auto chunks = {"{\"na", "me\": \"Ma", "ry\", \"l", "ist\": [1, 2", ", 3]}"};
for (const std::string chunk : chunks)
{
std::cout << "chunk " << chunk << " -> " << (push(chunk, value) ? "complete" : "incomplete") << '\n';
}
std::cout << value << '\n';
// a syntax error in the middle is not mistaken for missing data
try
{
push("[1, ]", value);
}
catch (const json::parse_error& e)
{
std::cout << e.what() << '\n';
}
}
@@ -0,0 +1,7 @@
chunk {"na -> incomplete
chunk me": "Ma -> incomplete
chunk ry", "l -> incomplete
chunk ist": [1, 2 -> incomplete
chunk , 3]} -> complete
{"list":[1,2,3],"name":"Mary"}
[json.exception.parse_error.101] parse error at line 1, column 5: syntax error while parsing value - unexpected ']'; expected '[', '{', or a literal
@@ -0,0 +1,23 @@
#include <iostream>
#include <sstream>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
int main()
{
// a CBOR value followed by other data
const std::vector<std::uint8_t> cbor = json::to_cbor(json{1, 2, 3});
std::string data(cbor.begin(), cbor.end());
data += "TRAILING";
std::istringstream input(data);
json j = json::from_cbor(input, false); // strict = false: stop after the first value
std::cout << "value: " << j << '\n';
std::cout << "bytes used: " << input.tellg() << '\n';
std::string rest;
input >> rest;
std::cout << "remaining input: " << rest << '\n';
}
@@ -0,0 +1,3 @@
value: [1,2,3]
bytes used: 4
remaining input: TRAILING
@@ -0,0 +1,18 @@
#include <iostream>
#include <sstream>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
int main()
{
std::istringstream input(R"({"a":1} [2,3] 4 "s")");
// check for the end of the stream before each read, because a read at the end throws
json j;
while (input >> std::ws && input.peek() != std::char_traits<char>::eof())
{
input >> j;
std::cout << j << '\n';
}
}
@@ -0,0 +1,4 @@
{"a":1}
[2,3]
4
"s"
@@ -0,0 +1,27 @@
#include <iostream>
#include <sstream>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
void handle(const json& record)
{
std::cout << "handling " << record << '\n';
}
int main()
{
std::istringstream file(R"([{"id": 1}, {"id": 2}, {"id": 3}])");
json result = json::parse(file, [](int depth, json::parse_event_t event, json & record)
{
if (depth == 1 && event == json::parse_event_t::object_end)
{
handle(record);
return false; // discard the record
}
return true;
});
std::cout << "result: " << result << '\n';
}
@@ -0,0 +1,4 @@
handling {"id":1}
handling {"id":2}
handling {"id":3}
result: []
@@ -0,0 +1,81 @@
#include <iostream>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
struct find_id : nlohmann::json_sax<json>
{
bool next_is_id = false;
json::number_unsigned_t id = 0;
bool key(std::string& name) override
{
next_is_id = (name == "id");
return true;
}
bool number_unsigned(json::number_unsigned_t value) override
{
if (next_is_id)
{
id = value;
return false; // found: stop parsing
}
return true;
}
bool null() override
{
return true;
}
bool boolean(bool /*val*/) override
{
return true;
}
bool number_integer(json::number_integer_t /*val*/) override
{
return true;
}
bool number_float(json::number_float_t /*val*/, const std::string& /*s*/) override
{
return true;
}
bool string(std::string& /*val*/) override
{
return true;
}
bool binary(json::binary_t& /*val*/) override
{
return true;
}
bool start_object(std::size_t /*elements*/) override
{
return true;
}
bool end_object() override
{
return true;
}
bool start_array(std::size_t /*elements*/) override
{
return true;
}
bool end_array() override
{
return true;
}
bool parse_error(std::size_t /*position*/, const std::string& /*last_token*/, const json::exception& /*ex*/) override
{
return false;
}
};
int main()
{
find_id handler;
const bool completed = json::sax_parse(R"({"x": [1, 2, 3], "id": 42, "rest": [4, 5, 6]})", &handler);
std::cout << std::boolalpha;
std::cout << "parse completed: " << completed << '\n';
std::cout << "id: " << handler.id << '\n';
}
@@ -0,0 +1,2 @@
parse completed: false
id: 42
@@ -0,0 +1,56 @@
#include <chrono>
#include <iostream>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
namespace nlohmann
{
template <typename Rep, typename Period>
struct adl_serializer<std::chrono::duration<Rep, Period>>
{
static void to_json(json& j, const std::chrono::duration<Rep, Period>& d)
{
j = d.count();
}
static void from_json(const json& j, std::chrono::duration<Rep, Period>& d)
{
d = std::chrono::duration<Rep, Period>(j.get<Rep>());
}
};
template <typename Duration>
struct adl_serializer<std::chrono::time_point<std::chrono::system_clock, Duration>>
{
static void to_json(json& j, const std::chrono::time_point<std::chrono::system_clock, Duration>& t)
{
j = t.time_since_epoch();
}
static void from_json(const json& j, std::chrono::time_point<std::chrono::system_clock, Duration>& t)
{
t = std::chrono::time_point<std::chrono::system_clock, Duration>(j.get<Duration>());
}
};
} // namespace nlohmann
int main()
{
using namespace std::chrono;
std::cout << std::boolalpha;
// the unit is not part of the JSON value
const json timeout = milliseconds(1500);
std::cout << "duration: " << timeout << '\n';
std::cout << "as milliseconds: " << timeout.get<milliseconds>().count() << '\n';
// a system_clock time point is the time since the Unix epoch
const time_point<system_clock, milliseconds> t(milliseconds(1700000000123));
const json stamp = t;
std::cout << "time point: " << stamp << '\n';
const auto back = stamp.get<time_point<system_clock, milliseconds>>();
std::cout << "round trip: " << (back == t) << '\n';
}
@@ -0,0 +1,4 @@
duration: 1500
as milliseconds: 1500
time point: 1700000000123
round trip: true
@@ -0,0 +1,99 @@
#include <iostream>
#include <memory>
#include <stdexcept>
#include <vector>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
struct shape
{
virtual ~shape() = default;
virtual json serialize() const = 0;
};
struct circle : shape
{
double radius;
explicit circle(double r) : radius(r) {}
json serialize() const override
{
return {{"type", "circle"}, {"radius", radius}};
}
};
struct rectangle : shape
{
double width;
double height;
rectangle(double w, double h) : width(w), height(h) {}
json serialize() const override
{
return {{"type", "rectangle"}, {"width", width}, {"height", height}};
}
};
namespace nlohmann
{
template <>
struct adl_serializer<std::unique_ptr<shape>>
{
static void to_json(json& j, const std::unique_ptr<shape>& p)
{
j = p ? p->serialize() : json(nullptr);
}
static void from_json(const json& j, std::unique_ptr<shape>& p)
{
if (j.is_null())
{
p.reset();
return;
}
const auto& type = j.at("type").get_ref<const std::string&>();
if (type == "circle")
{
p = std::make_unique<circle>(j.at("radius").get<double>());
}
else if (type == "rectangle")
{
p = std::make_unique<rectangle>(j.at("width").get<double>(), j.at("height").get<double>());
}
else
{
throw std::invalid_argument("unknown shape type: " + type);
}
}
};
} // namespace nlohmann
int main()
{
std::vector<std::unique_ptr<shape>> shapes;
shapes.push_back(std::make_unique<circle>(1.5));
shapes.push_back(std::make_unique<rectangle>(2, 3));
const json j = shapes;
std::cout << j << '\n';
// reading creates objects of the derived types
const auto back = j.get<std::vector<std::unique_ptr<shape>>>();
std::cout << std::boolalpha;
std::cout << "first is a circle: " << (dynamic_cast<circle*>(back[0].get()) != nullptr) << '\n';
std::cout << "second is a rectangle: " << (dynamic_cast<rectangle*>(back[1].get()) != nullptr) << '\n';
// an unknown type tag is reported
try
{
json{{"type", "star"}}.get<std::unique_ptr<shape>>();
}
catch (const std::invalid_argument& e)
{
std::cout << e.what() << '\n';
}
}
@@ -0,0 +1,4 @@
[{"radius":1.5,"type":"circle"},{"height":3.0,"type":"rectangle","width":2.0}]
first is a circle: true
second is a rectangle: true
unknown shape type: star
@@ -0,0 +1,90 @@
#include <iostream>
#include <memory>
#include <vector>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
namespace nlohmann
{
template <typename T>
struct adl_serializer<std::shared_ptr<T>>
{
static void to_json(json& j, const std::shared_ptr<T>& p)
{
if (p)
{
j = *p;
}
else
{
j = nullptr;
}
}
static void from_json(const json& j, std::shared_ptr<T>& p)
{
if (j.is_null())
{
p = nullptr;
}
else
{
p = std::make_shared<T>(j.get<typename std::remove_const<T>::type>());
}
}
};
template <typename T>
struct adl_serializer<std::unique_ptr<T>>
{
static void to_json(json& j, const std::unique_ptr<T>& p)
{
if (p)
{
j = *p;
}
else
{
j = nullptr;
}
}
static void from_json(const json& j, std::unique_ptr<T>& p)
{
if (j.is_null())
{
p = nullptr;
}
else
{
p = std::make_unique<T>(j.get<T>());
}
}
};
} // namespace nlohmann
struct point
{
int x;
int y;
};
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(point, x, y)
int main()
{
// an empty pointer is written as null
const std::vector<std::shared_ptr<point>> points = {std::make_shared<point>(point{1, 2}), nullptr};
const json j = points;
std::cout << j << '\n';
// reading allocates new objects
const auto back = j.get<std::vector<std::shared_ptr<point>>>();
std::cout << back[0]->x << ", " << back[0]->y << ", " << (back[1] == nullptr) << '\n';
// pointers to const and unique_ptr work as well
const auto const_ptr = j[0].get<std::shared_ptr<const point>>();
const auto unique = j[0].get<std::unique_ptr<point>>();
std::cout << const_ptr->y << ", " << unique->x << '\n';
}
@@ -0,0 +1,3 @@
[{"x":1,"y":2},null]
1, 2, 1
2, 1
@@ -0,0 +1,63 @@
#include <iostream>
#include <stdexcept>
#include <variant>
#include <vector>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
namespace geo
{
struct circ
{
double radius;
};
struct rect
{
double width;
double height;
};
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(circ, radius)
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(rect, width, height)
using figure = std::variant<circ, rect>;
void to_json(json& j, const figure& f)
{
std::visit([&j](const auto & alternative)
{
j = alternative;
}, f);
j["type"] = std::holds_alternative<circ>(f) ? "circ" : "rect";
}
void from_json(const json& j, figure& f)
{
const auto& type = j.at("type").get_ref<const std::string&>();
if (type == "circ")
{
f = j.get<circ>();
}
else if (type == "rect")
{
f = j.get<rect>();
}
else
{
throw std::invalid_argument("unknown figure type: " + type);
}
}
} // namespace geo
int main()
{
const std::vector<geo::figure> figures = {geo::circ{2}, geo::rect{3, 4}};
const json j = figures;
std::cout << j << '\n';
const auto back = j.get<std::vector<geo::figure>>();
std::cout << "alternatives read back: " << back[0].index() << ", " << back[1].index() << '\n';
}
@@ -0,0 +1,2 @@
[{"radius":2.0,"type":"circ"},{"height":4.0,"type":"rect","width":3.0}]
alternatives read back: 0, 1