* Parse JSON in one block of memory with a fused pointer-based parser The parser and the lexer exchange one token at a time, and the lexer keeps its state (the current character, a pending unget, where the token starts) in members, which every character pays for: json::accept() of twitter.json took 2.8x as long as json_document::accept(). detail::contiguous_parser reads text in one block of memory through a local pointer, with the grammar, the whitespace and the tokens in one loop (after yyjson's reader), and drives a SAX handler: parse()'s DOM handler, accept()'s handler, or the user's handler of sax_parse(). It is used with the default options (no callback, no comments, no trailing commas; sax_parse() with strict = true). Errors are reported by the library parser, so the exception, its message and its position stay the same. contiguous_parser stops at the token that the library parser would reject, before reporting it, so the events up to the error are the same; then: - parse() runs the library parser with a handler that only reports the error (json_sax_error_reporter), with the lexer of parse(), which is instantiated anyway; - sax_parse() runs it with a handler that forwards only parse_error() to the user's handler (json_sax_error_forwarder), so no event arrives twice; - accept() returns the verdict. A handler that returns false stops the parse as before. An error at the end of a large input costs parse() about 1.2x of what it cost before. On an Apple M1 Max: accept() 1.6-2.0x as fast, parse() 10-16% faster, ordered_json 11-16%, sax_parse() with a user's handler 1.36-1.46x, small messages 12-22% (parse()) and 2.1-2.4x (accept()). On x86-64 (GCC 15, Clang 21): accept() 1.6-1.9x, parse() 5-17%, ordered_json 12-19%, sax_parse() 1.32-1.57x. A translation unit that parses a string compiles 3% (-O0) and 9% (-O2) slower. unit-contiguous_parser.cpp compares the contiguous path with the byte path (std::list iterators) on corner cases, generated documents, their truncations and mutations, deep nesting, and narrow number types: the same values with the same number types and float bits, exception messages, accept() verdicts and sax_parse() events. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Fix CI: unused parameter without exceptions, cpplint, BUILD.bazel - json_sax_error_reporter::parse_error: mark the exception unused, as JSON_THROW does not use it with JSON_NOEXCEPTION - unit-contiguous_parser.cpp: compare the exceptions of both paths only when exceptions are enabled (invalid input aborted the test otherwise) - silence cpplint's readability/braces false positives on the if statements inside the parser's macros - add contiguous_parser.hpp to BUILD.bazel Signed-off-by: Niels Lohmann <mail@nlohmann.me> * accept(): skip digits without computing their value accept() needs the value of a number only for a float whose exponent may reach the largest value. skip_digits() finds the end of the digits without the multiply-adds of accumulate_digits(), and once an 8-byte word fails the digit test, the remaining loop needs no bounds check: a non-digit is in that word. The value is computed afterwards for the rare floats that need it. accept() of canada.json: -16 % on Apple M1, -22 % with GCC 15 and -21 % with Clang 21 on x86-64; files with many numbers -9 to -15 %. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Tests: mark parser callback in unit-contiguous_parser noexcept Fixes the -Werror=noexcept failure with GCC 26 (ci_test_standards_gcc (26)). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N5NasEsrzHhSNouxSoEwqz Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Tests: sweep_allocator::construct() accepts pointers to const Recent libc++ (emsdk latest) probes construct(const T*, const T&) in an unevaluated context while checking for trivial copies; the static_cast to void* in the noexcept specification then fails to compile. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N5NasEsrzHhSNouxSoEwqz (cherry picked from commit 794bf70823c12a2a1cb2a8a95f34dfb6357746a8) Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Tests: guard sweep_allocator::construct() against const types instead of const_cast The const_cast added in the previous commit is rejected by cppcoreguidelines-pro-type-const-cast (clang-tidy). Make construct() not viable for const U instead, which is what libc++ probes for. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N5NasEsrzHhSNouxSoEwqz (cherry picked from commit de099fa607001b710862fd1bbecef3609c54cbc4) Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Fix CI: astyle and clang-tidy for sweep_allocator::construct() astyle 3.4.13 reformats the enable_if<!...> default template argument (Check amalgamation). Delete the const overload instead, which makes the libc++ probe for const objects fail in the SFINAE context just the same and needs no <type_traits>. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N5NasEsrzHhSNouxSoEwqz Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Contiguous parser: review fixes (wording, macros, diagnostic positions) - "default parser" instead of "library parser" in comments and tests - the three comments in json.hpp say the same thing - current() replaces the repeated `p != end ? *p : 0` - the backslashes of each macro are aligned - the docs say that JSON_DIAGNOSTIC_POSITIONS turns the fast path off Signed-off-by: Niels Lohmann <mail@nlohmann.me> --------- Signed-off-by: Niels Lohmann <mail@nlohmann.me> Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
5.8 KiB
Reading and writing files
The library has no functions that take a file name. It reads from and writes to streams, so files are handled with
#!cpp std::ifstream and #!cpp std::ofstream (or a #!cpp FILE* when reading). This page collects the recipes for
the common cases and the mistakes that show up again and again.
Reading a JSON file
Pass the stream to parse directly, and check that the file was opened:
#include <fstream>
#include <stdexcept>
#include <string>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
json read_json_file(const std::string& path)
{
std::ifstream f(path);
if (!f.is_open())
{
throw std::runtime_error("cannot open " + path);
}
return json::parse(f);
}
- Check that the file is open. A stream that failed to open looks like an empty input, so
parsereportsparse_error.101("unexpected end of input") at line 1, column 1 instead of saying that the file is missing. A relative path is resolved against the working directory of the running program, which is often not the directory of the source file (see the FAQ). - Read the file into a string only if the memory is no concern. Parsing from a
#!cpp std::ifstreamtakes about 1.15 to 1.45 times as long as reading the whole file into a string with oneread()and parsing that, but it needs no memory for the copy (see input sources and performance). The common way to read a file with#!cpp std::istreambuf_iterator<char>is so slow that it takes about twice as long as parsing from the stream. This advice is for#!cpp json::parse.json_documentreads a stream into a buffer that it owns anyway, so there is no copy to save: reading the file into a#!cpp std::stringyourself and passing that as an lvalue, which the document borrows, was about 6 to 12 % faster than parsing from the stream, with the same single buffer. parserequires the file to hold exactly one JSON value. Files with one value per line are read with JSON Lines; files too large for memory are covered in Large, streamed, and partial input.
Writing a JSON file
dump the value into a string first, then write the string:
void write_json_file(const std::string& path, const json& j)
{
const std::string text = j.dump(4) + '\n'; // may throw: nothing is written yet
std::ofstream f(path, std::ios::binary);
if (!f.is_open())
{
throw std::runtime_error("cannot open " + path);
}
f << text;
f.close();
if (f.fail())
{
throw std::runtime_error("cannot write " + path);
}
}
- Serialize before opening the file. Opening a
#!cpp std::ofstreamtruncates the file.dumpandoperator<<throwtype_error.316when a string is not valid UTF-8 (see handling invalid UTF-8), and#!cpp f << std::setw(4) << jmay already have written part of the value when it throws. Serializing into a string first leaves the old file untouched in that case. - Check the stream after closing it. A full disk or a missing permission sets the stream's error state; it does not
throw unless you enabled exceptions on the stream with
exceptions(). std::ios::binarywrites the text as it is. Without it, Windows writes\r\nfor every newline of a pretty-printed value.
Replacing a file without corrupting it
The library never writes a file itself, so this section is not about the library. It is general C++ advice for a problem users of the library keep running into.
A program that crashes or is killed while it writes leaves a truncated file behind. To make sure readers see either the
old or the new contents, write to a temporary file in the same directory and rename it over the original
(C++17, #!cpp <filesystem>):
#include <filesystem>
void replace_json_file(const std::filesystem::path& path, const json& j)
{
std::filesystem::path tmp = path;
tmp += ".tmp"; // use a unique name if several processes may write the file
write_json_file(tmp.string(), j);
std::filesystem::rename(tmp, path); // replaces path
}
The temporary file must be in the same directory (more precisely, on the same file system) as the target: a rename
across file systems does not fall back to a copy, it fails and throws #!cpp std::filesystem::filesystem_error. On
POSIX systems, the rename atomically replaces the file. Standard C++ has no way to flush a file to the disk (fsync),
so this protects against a crashed program, but not necessarily against a power loss.
Binary formats
The binary formats work the same way, with the streams opened in binary mode:
// read
std::ifstream in("data.cbor", std::ios::binary);
json j = json::from_cbor(in);
// write
const std::vector<std::uint8_t> bytes = json::to_cbor(j);
std::ofstream out("data.cbor", std::ios::binary);
out.write(reinterpret_cast<const char*>(bytes.data()), static_cast<std::streamsize>(bytes.size()));
Without std::ios::binary, Windows turns the bytes 0x0D 0x0A into 0x0A and may stop at a byte 0x1A while reading,
and turns 0x0A into 0x0D 0x0A while writing, which corrupts the data.
See also
- Parsing - the inputs
parseaccepts - Serialization - the options of
dump parse,dumpoperator>>,operator<<