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 std::ifstream and std::ofstream (or a 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). - Don't read the file into a string first. Parsing from a
std::ifstreamis only about 10 to 20 % slower than reading the whole file into a string with oneread()and parsing that, and it needs no memory for the copy (see input sources and performance). The common way to read a file withstd::istreambuf_iterator<char>is so slow that it takes about twice as long as parsing from the stream. Read the file into a string only if you need the text itself, for example for an error message. This advice is forjson::parse.json_documentreads a stream into a buffer that it owns anyway, so there is no copy to save: reading the file into astd::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
std::ofstreamtruncates the file.dumpandoperator<<throwtype_error.316when a string is not valid UTF-8 (see handling invalid UTF-8), andf << 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, <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 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<<