Compare commits

..
Author SHA1 Message Date
Niels Lohmann 379756e92e json_view: no byte range in exceptions about a removed value
#5990 dropped the found flag from diagnostics(), reasoning that a value
value_path() does not find leaves no tokens, like the root. But the node
of a value that is no longer part of the document still holds the bytes
it had in the source, so with JSON_DIAGNOSTICS and
JSON_DIAGNOSTIC_POSITIONS its exceptions gained "(bytes 1-4)" for text
that is no longer there. The test #5997 added for exactly this case
(unit-json_view_positions.cpp, "views of values that are no longer part
of the document") has failed on develop since both were merged.

Restore the flag: a value that is not found gets no context, the root
keeps its byte range.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-11-02 19:08:09 +01:00
12 changed files with 31 additions and 170 deletions

No files matched your search

@@ -15,9 +15,7 @@ the patch. This function applies a JSON patch in place and returns void.
## Exception safety ## Exception safety
No guarantees: if an operation fails, the operations before it stay applied, and a failing `move` operation may already No guarantees, value may be corrupted by an unsuccessful patch operation.
have removed the value at its "from" location. Use [`patch`](patch.md) if the value must be unchanged after a failed
patch.
## Exceptions ## Exceptions
@@ -64,19 +62,8 @@ the JSON value, the complexity can usually be neglected.
## Notes ## Notes
Unlike [`patch`](patch.md), `patch_inplace` applies the operation "in place" and no copy of the JSON value is created. Unlike [`patch`](patch.md), `patch_inplace` applies the operation "in place" and no copy of the JSON value is created.
That makes it faster for large documents by avoiding the copy. That makes it faster for large documents by avoiding the copy. However, the JSON value might be corrupted if the
function throws an exception.
!!! warning "Not atomic"
`patch_inplace` applies the operations one after the other and does not undo them when one fails. After an
exception, the value holds the result of the operations before the failing one, and a failing `move` operation
(a "remove" followed by an "add") may already have removed the value at its "from" location; see the second example
below. [RFC 6902](https://tools.ietf.org/html/rfc6902#section-5) requires that a patch with a failing operation is
not deemed successful, but leaves open what happens to the document.
If the value must stay unchanged when the patch fails, use [`patch`](patch.md), which applies the patch to a copy:
`#!cpp j = j.patch(json_patch);`. Validating the patch beforehand is not enough, as "test" operations and missing
paths are only detected while the patch is applied.
Duplicate member names inside an operation object are resolved by the parser before the patch is applied (the last one Duplicate member names inside an operation object are resolved by the parser before the patch is applied (the last one
is kept, see [`object_t`](object_t.md#behavior)), so `#!json {"op": "add", "op": "remove", "path": "/a"}` is a is kept, see [`object_t`](object_t.md#behavior)), so `#!json {"op": "add", "op": "remove", "path": "/a"}` is a
-123
View File
@@ -1,123 +0,0 @@
# 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`](../api/basic_json/parse.md) directly, and check that the file was opened:
```cpp
#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`](../home/exceptions.md#jsonexceptionparse_error101) ("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](../home/faq.md#parsing-a-file-fails-with-unexpected-end-of-input)).
- **Don't read the file into a string first.** Parsing from a `#!cpp std::ifstream` is only about 10 to 20 % slower
than parsing a string that holds the whole file, and it needs no memory for the copy (see
[input sources and performance](parsing/index.md#input-sources-and-performance)). Read the file into a string only if
you need the text itself, for example for an error message.
- `parse` requires the file to hold exactly one JSON value. Files with one value per line are read with
[JSON Lines](parsing/json_lines.md); files too large for memory are covered in
[Large, streamed, and partial input](parsing/large_and_partial_input.md).
## Writing a JSON file
[`dump`](../api/basic_json/dump.md) the value into a string first, then write the string:
```cpp
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<<`](../api/operator_ltlt.md) throw [`type_error.316`](../home/exceptions.md#jsonexceptiontype_error316)
when a string is not valid UTF-8 (see [handling invalid UTF-8](serialization.md#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
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>`):
```cpp
#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, as a rename
across file systems is a copy. 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](binary_formats/index.md) work the same way, with the streams opened in binary mode:
```cpp
// 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](parsing/index.md) - the inputs `parse` accepts
- [Serialization](serialization.md) - the options of `dump`
- [`parse`](../api/basic_json/parse.md), [`dump`](../api/basic_json/dump.md)
- [`operator>>`](../api/operator_gtgt.md), [`operator<<`](../api/operator_ltlt.md)
-2
View File
@@ -46,8 +46,6 @@ C++ types, and finally serialize it again.
[BJData](binary_formats/bjdata.md), [BON8](binary_formats/bon8.md), [BSON](binary_formats/bson.md), [BJData](binary_formats/bjdata.md), [BON8](binary_formats/bon8.md), [BSON](binary_formats/bson.md),
[CBOR](binary_formats/cbor.md), [MessagePack](binary_formats/messagepack.md), or [UBJSON](binary_formats/ubjson.md). [CBOR](binary_formats/cbor.md), [MessagePack](binary_formats/messagepack.md), or [UBJSON](binary_formats/ubjson.md).
- [Binary values](binary_values.md) — store and exchange raw byte sequences. - [Binary values](binary_values.md) — store and exchange raw byte sequences.
- [Reading and writing files](files.md) — read and write files safely, including replacing a file without
corrupting it.
## How values are stored and configured ## How values are stored and configured
+1 -2
View File
@@ -29,8 +29,7 @@ other three continue on its result.
[`patch`](../api/basic_json/patch.md) leaves the original value unchanged and returns the patched result as a copy. [`patch`](../api/basic_json/patch.md) leaves the original value unchanged and returns the patched result as a copy.
If the document is large and the original value is no longer needed, If the document is large and the original value is no longer needed,
[`patch_inplace`](../api/basic_json/patch_inplace.md) applies the same operations in place instead. Unlike [`patch_inplace`](../api/basic_json/patch_inplace.md) applies the same operations in place instead.
`patch`, it is not atomic: if an operation fails, the operations before it stay applied.
!!! note "Duplicate member names" !!! note "Duplicate member names"
@@ -35,8 +35,6 @@ std::ifstream f("example.json");
json data = json::parse(f); json data = json::parse(f);
``` ```
See [Reading and writing files](../files.md) for checking that the file was opened and for writing files safely.
The input must be encoded in UTF-8; other encodings are not supported. The exception is wide-character input, which The input must be encoded in UTF-8; other encodings are not supported. The exception is wide-character input, which
is read as UTF-16 or UTF-32 — depending on the character type — and converted to UTF-8; see the is read as UTF-16 or UTF-32 — depending on the character type — and converted to UTF-8; see the
[`parse`](../../api/basic_json/parse.md) page for an example. A single input may contain only one JSON value. [`parse`](../../api/basic_json/parse.md) page for an example. A single input may contain only one JSON value.
@@ -17,9 +17,6 @@ To write a value directly to a stream (for example, a file or `#!cpp std::cout`)
std::cout << j << std::endl; std::cout << j << std::endl;
``` ```
To write a file without leaving a truncated file behind when serialization fails, see
[Reading and writing files](files.md#writing-a-json-file).
!!! note "String, not raw value" !!! note "String, not raw value"
`dump` always returns a **JSON text**. Serializing a JSON string therefore includes the surrounding quotes and `dump` always returns a **JSON text**. Serializing a JSON string therefore includes the surrounding quotes and
+1 -1
View File
@@ -110,7 +110,7 @@ in the source files that need it, instantiate `basic_json` once, and consider pr
The parser did not see any character: the file was most likely not opened. A relative path is resolved against the The parser did not see any character: the file was most likely not opened. A relative path is resolved against the
working directory of the running program, which is often the build directory of your IDE, not the source directory. working directory of the running program, which is often the build directory of your IDE, not the source directory.
Check `is_open()` and try an absolute path. Other causes are an empty file and a stream that was already read. See Check `is_open()` and try an absolute path. Other causes are an empty file and a stream that was already read. See
[`parse_error.101`](exceptions.md#jsonexceptionparse_error101) and [Reading and writing files](../features/files.md). [`parse_error.101`](exceptions.md#jsonexceptionparse_error101).
### Input that arrives in chunks ### Input that arrives in chunks
-1
View File
@@ -130,7 +130,6 @@ nav:
- features/element_access/unchecked_access.md - features/element_access/unchecked_access.md
- features/element_access/checked_access.md - features/element_access/checked_access.md
- features/element_access/default_value.md - features/element_access/default_value.md
- features/files.md
- features/iterators.md - features/iterators.md
- features/merge_patch.md - features/merge_patch.md
- features/json_patch.md - features/json_patch.md
+6 -6
View File
@@ -1168,20 +1168,20 @@ indent_done:
if (negative) if (negative)
{ {
++p; ++p;
if (NLOHMANN_VIEW_UNLIKELY(cur() < '0' || cur() > '9'))
{
return fail(error_code::number_after_minus);
}
} }
const unsigned char* const int_start = p; const unsigned char* const int_start = p;
if (cur() == '0') if (cur() == '0')
{ {
++p; ++p;
} }
else if (!negative || NLOHMANN_VIEW_LIKELY(cur() >= '1' && cur() <= '9'))
{
// (without a sign, the dispatch saw a digit)
p = skip_digits(p + 1, e);
}
else else
{ {
// (a digit: the dispatch or the check after the sign saw it) return fail(error_code::number_after_minus);
p = skip_digits(p + 1, e);
} }
const auto int_digits = static_cast<std::size_t>(p - int_start); const auto int_digits = static_cast<std::size_t>(p - int_start);
std::size_t frac_digits = 0; std::size_t frac_digits = 0;
+5 -2
View File
@@ -230,7 +230,8 @@ void value_range(const document_data& d, const node* n, std::size_t& start, std:
/// the context basic_json puts in front of the message of an exception about /// the context basic_json puts in front of the message of an exception about
/// a value: "(/path) " with JSON_DIAGNOSTICS (none for the root), then /// a value: "(/path) " with JSON_DIAGNOSTICS (none for the root), then
/// "(bytes a-b) " with JSON_DIAGNOSTIC_POSITIONS (also for the root) /// "(bytes a-b) " with JSON_DIAGNOSTIC_POSITIONS (also for the root); with
/// JSON_DIAGNOSTICS, nothing for a value that is no longer part of the document
/// ///
/// This and the throw helpers that take an error_site are templates, so that /// This and the throw helpers that take an error_site are templates, so that
/// the path and range code is instantiated only where an exception can be /// the path and range code is instantiated only where an exception can be
@@ -251,10 +252,12 @@ std::string diagnostics(const error_site site)
std::string context; std::string context;
#if JSON_DIAGNOSTICS #if JSON_DIAGNOSTICS
std::vector<std::string> tokens; std::vector<std::string> tokens;
// a value that is no longer part of the document has no path, and its
// node still holds the bytes it had in the source: no context at all
const bool found = edited ? value_path<true>(d, site.value(), tokens) : value_path<false>(d, site.value(), tokens); const bool found = edited ? value_path<true>(d, site.value(), tokens) : value_path<false>(d, site.value(), tokens);
if (!found) if (!found)
{ {
// a value that is not found has no path, and with it no bytes // context is still empty; returning it keeps the copy elided (-Wnrvo)
return context; return context;
} }
if (!tokens.empty()) if (!tokens.empty())
+11 -8
View File
@@ -2473,20 +2473,20 @@ indent_done:
if (negative) if (negative)
{ {
++p; ++p;
if (NLOHMANN_VIEW_UNLIKELY(cur() < '0' || cur() > '9'))
{
return fail(error_code::number_after_minus);
}
} }
const unsigned char* const int_start = p; const unsigned char* const int_start = p;
if (cur() == '0') if (cur() == '0')
{ {
++p; ++p;
} }
else if (!negative || NLOHMANN_VIEW_LIKELY(cur() >= '1' && cur() <= '9'))
{
// (without a sign, the dispatch saw a digit)
p = skip_digits(p + 1, e);
}
else else
{ {
// (a digit: the dispatch or the check after the sign saw it) return fail(error_code::number_after_minus);
p = skip_digits(p + 1, e);
} }
const auto int_digits = static_cast<std::size_t>(p - int_start); const auto int_digits = static_cast<std::size_t>(p - int_start);
std::size_t frac_digits = 0; std::size_t frac_digits = 0;
@@ -5059,7 +5059,8 @@ void value_range(const document_data& d, const node* n, std::size_t& start, std:
/// the context basic_json puts in front of the message of an exception about /// the context basic_json puts in front of the message of an exception about
/// a value: "(/path) " with JSON_DIAGNOSTICS (none for the root), then /// a value: "(/path) " with JSON_DIAGNOSTICS (none for the root), then
/// "(bytes a-b) " with JSON_DIAGNOSTIC_POSITIONS (also for the root) /// "(bytes a-b) " with JSON_DIAGNOSTIC_POSITIONS (also for the root); with
/// JSON_DIAGNOSTICS, nothing for a value that is no longer part of the document
/// ///
/// This and the throw helpers that take an error_site are templates, so that /// This and the throw helpers that take an error_site are templates, so that
/// the path and range code is instantiated only where an exception can be /// the path and range code is instantiated only where an exception can be
@@ -5080,10 +5081,12 @@ std::string diagnostics(const error_site site)
std::string context; std::string context;
#if JSON_DIAGNOSTICS #if JSON_DIAGNOSTICS
std::vector<std::string> tokens; std::vector<std::string> tokens;
// a value that is no longer part of the document has no path, and its
// node still holds the bytes it had in the source: no context at all
const bool found = edited ? value_path<true>(d, site.value(), tokens) : value_path<false>(d, site.value(), tokens); const bool found = edited ? value_path<true>(d, site.value(), tokens) : value_path<false>(d, site.value(), tokens);
if (!found) if (!found)
{ {
// a value that is not found has no path, and with it no bytes // context is still empty; returning it keeps the copy elided (-Wnrvo)
return context; return context;
} }
if (!tokens.empty()) if (!tokens.empty())
+4 -4
View File
@@ -505,8 +505,8 @@ struct locale_switching_token
{ {
using value_type = char; using value_type = char;
locale_switching_token(std::string text_, std::string switch_to_) locale_switching_token(std::string text_, const char* switch_to_)
: text(std::move(text_)), switch_to(std::move(switch_to_)) : text(std::move(text_)), switch_to(switch_to_)
{} {}
std::size_t size() const std::size_t size() const
@@ -523,13 +523,13 @@ struct locale_switching_token
{ {
if (!switched) if (!switched)
{ {
switched = std::setlocale(LC_NUMERIC, switch_to.c_str()) != nullptr; switched = std::setlocale(LC_NUMERIC, switch_to) != nullptr;
} }
return &text[0]; return &text[0];
} }
std::string text; std::string text;
std::string switch_to; const char* switch_to;
bool switched = false; bool switched = false;
}; };