Files
json/docs/mkdocs/docs/features/files.md
T
Niels LohmannandClaude Sonnet 5.5 33f15727ce Parse JSON in one block of memory with a fused pointer-based parser (#6031)
* 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>
2026-11-10 07:08:44 +01:00

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 parse reports parse_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::ifstream takes about 1.15 to 1.45 times as long as reading the whole file into a string with one read() 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_document reads a stream into a buffer that it owns anyway, so there is no copy to save: reading the file into a #!cpp std::string yourself 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.
  • parse requires 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::ofstream truncates the file. dump and operator<< throw type_error.316 when a string is not valid UTF-8 (see handling invalid UTF-8), and #!cpp f << std::setw(4) << j may 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::binary writes the text as it is. Without it, Windows writes \r\n for 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