Compare commits

..
Author SHA1 Message Date
Niels Lohmann 8c1421d493 Merge develop into fix/issue-5993
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-11-02 20:07:52 +01:00
Niels Lohmann 6d8d2df367 Merge develop into fix/issue-5993
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-11-02 18:27:02 +01:00
Niels Lohmann 6aa10fd343 Simplify the decimal point condition in dump_float
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-11-02 17:25:51 +01:00
Niels Lohmann 98cfd55317 Move the fraction only for a multibyte decimal point
For a one-byte decimal point, the std::copy() call had its destination at
the start of its source range, which std::copy() does not allow. The bytes
are now moved only when the decimal point is longer than the '.' it is
replaced with. Also name the right place of get_decimal_point().

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-11-02 09:32:50 +01:00
Claude e96469f783 Use std::char_traits<char>::length instead of strlen in dump_float()
Codacy (flawfinder, CWE-126) flags strlen on the locale's decimal point.
The string is NUL-terminated by localeconv(), so the behavior is the same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N5NasAgbDGG28MqXjQvLqp
Signed-off-by: Claude <noreply@anthropic.com>
2026-11-02 05:39:29 +00:00
Niels Lohmann 3720e4b3aa Replace a multibyte decimal point in dump() of a long double (#5993)
The snprintf path of dump_float(), used when number_float_t is neither
IEEE single nor double (a long double on x86-64 and aarch64 Linux), read
localeconv()->decimal_point and ->thousands_sep as single chars. Under a
locale with a multibyte decimal point (glibc's ps_AF: U+066B), it removed
the first byte as a "thousands separator" and left the second one in the
output, so 1.5L dumped as "1\xab5.0".

Find the whole decimal point with std::search and replace it with '.',
as the parser already does since #5660. Drop the thousands-separator
removal: %g never groups digits.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-11-01 22:48:42 +01:00
11 changed files with 59 additions and 188 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
+23 -19
View File
@@ -8,7 +8,7 @@
#pragma once #pragma once
#include <algorithm> // remove, fill, find, none_of, min #include <algorithm> // copy, none_of, min, search
#include <array> // array #include <array> // array
#include <clocale> // localeconv, lconv #include <clocale> // localeconv, lconv
#include <cmath> // isfinite #include <cmath> // isfinite
@@ -2080,31 +2080,35 @@ class serializer
// check if the buffer was large enough // check if the buffer was large enough
JSON_ASSERT(static_cast<std::size_t>(len) < number_buffer.size()); JSON_ASSERT(static_cast<std::size_t>(len) < number_buffer.size());
// look up the locale's thousands separator and decimal point now, // look up the locale's decimal point now, matching what
// matching what snprintf_float() just used (see lexer::get_decimal_point()) // snprintf_float() just used (see get_decimal_point() in
// number_parse.hpp); it is a string, which can be longer than one
// byte (U+066B in ps_AF, #5993).
// A thousands separator needs no handling: %g never groups digits.
const auto* loc = std::localeconv(); const auto* loc = std::localeconv();
JSON_ASSERT(loc != nullptr); JSON_ASSERT(loc != nullptr);
const char thousands_sep = (loc->thousands_sep == nullptr) ? '\0' : *loc->thousands_sep; const char* const decimal_point = (loc->decimal_point == nullptr) ? "" : loc->decimal_point;
const char decimal_point = (loc->decimal_point == nullptr) ? '\0' : *loc->decimal_point; const std::size_t decimal_point_len = std::char_traits<char>::length(decimal_point);
// erase thousands separators
if (thousands_sep != '\0')
{
// NOLINTNEXTLINE(readability-qualified-auto,llvm-qualified-auto): std::remove returns an iterator, see https://github.com/nlohmann/json/issues/3081
const auto end = std::remove(number_buffer.begin(), number_buffer.begin() + len, thousands_sep);
std::fill(end, number_buffer.end(), '\0');
JSON_ASSERT((end - number_buffer.begin()) <= len);
len = (end - number_buffer.begin());
}
// convert decimal point to '.' // convert decimal point to '.'
if (decimal_point != '\0' && decimal_point != '.') if ((decimal_point_len > 1) || (decimal_point_len == 1 && decimal_point[0] != '.'))
{ {
// NOLINTNEXTLINE(readability-qualified-auto,llvm-qualified-auto): std::find returns an iterator, see https://github.com/nlohmann/json/issues/3081 const auto first = number_buffer.begin();
const auto dec_pos = std::find(number_buffer.begin(), number_buffer.end(), decimal_point); const auto last = first + len;
if (dec_pos != number_buffer.end()) // NOLINTNEXTLINE(readability-qualified-auto,llvm-qualified-auto): std::search returns an iterator, see https://github.com/nlohmann/json/issues/3081
const auto dec_pos = std::search(first, last, decimal_point, decimal_point + decimal_point_len);
if (dec_pos != last)
{ {
*dec_pos = '.'; *dec_pos = '.';
if (decimal_point_len > 1)
{
// move the fraction and exponent behind the '.' (std::copy
// allows the overlap, as the destination is before the source)
// NOLINTNEXTLINE(readability-qualified-auto,llvm-qualified-auto): std::copy returns an iterator, see https://github.com/nlohmann/json/issues/3081
const auto end = std::copy(dec_pos + static_cast<std::ptrdiff_t>(decimal_point_len), last, dec_pos + 1);
*end = '\0';
len = end - first;
}
} }
} }
+23 -19
View File
@@ -33446,7 +33446,7 @@ NLOHMANN_JSON_NAMESPACE_END
#include <algorithm> // remove, fill, find, none_of, min #include <algorithm> // copy, none_of, min, search
#include <array> // array #include <array> // array
#include <clocale> // localeconv, lconv #include <clocale> // localeconv, lconv
#include <cmath> // isfinite #include <cmath> // isfinite
@@ -37300,31 +37300,35 @@ class serializer
// check if the buffer was large enough // check if the buffer was large enough
JSON_ASSERT(static_cast<std::size_t>(len) < number_buffer.size()); JSON_ASSERT(static_cast<std::size_t>(len) < number_buffer.size());
// look up the locale's thousands separator and decimal point now, // look up the locale's decimal point now, matching what
// matching what snprintf_float() just used (see lexer::get_decimal_point()) // snprintf_float() just used (see get_decimal_point() in
// number_parse.hpp); it is a string, which can be longer than one
// byte (U+066B in ps_AF, #5993).
// A thousands separator needs no handling: %g never groups digits.
const auto* loc = std::localeconv(); const auto* loc = std::localeconv();
JSON_ASSERT(loc != nullptr); JSON_ASSERT(loc != nullptr);
const char thousands_sep = (loc->thousands_sep == nullptr) ? '\0' : *loc->thousands_sep; const char* const decimal_point = (loc->decimal_point == nullptr) ? "" : loc->decimal_point;
const char decimal_point = (loc->decimal_point == nullptr) ? '\0' : *loc->decimal_point; const std::size_t decimal_point_len = std::char_traits<char>::length(decimal_point);
// erase thousands separators
if (thousands_sep != '\0')
{
// NOLINTNEXTLINE(readability-qualified-auto,llvm-qualified-auto): std::remove returns an iterator, see https://github.com/nlohmann/json/issues/3081
const auto end = std::remove(number_buffer.begin(), number_buffer.begin() + len, thousands_sep);
std::fill(end, number_buffer.end(), '\0');
JSON_ASSERT((end - number_buffer.begin()) <= len);
len = (end - number_buffer.begin());
}
// convert decimal point to '.' // convert decimal point to '.'
if (decimal_point != '\0' && decimal_point != '.') if ((decimal_point_len > 1) || (decimal_point_len == 1 && decimal_point[0] != '.'))
{ {
// NOLINTNEXTLINE(readability-qualified-auto,llvm-qualified-auto): std::find returns an iterator, see https://github.com/nlohmann/json/issues/3081 const auto first = number_buffer.begin();
const auto dec_pos = std::find(number_buffer.begin(), number_buffer.end(), decimal_point); const auto last = first + len;
if (dec_pos != number_buffer.end()) // NOLINTNEXTLINE(readability-qualified-auto,llvm-qualified-auto): std::search returns an iterator, see https://github.com/nlohmann/json/issues/3081
const auto dec_pos = std::search(first, last, decimal_point, decimal_point + decimal_point_len);
if (dec_pos != last)
{ {
*dec_pos = '.'; *dec_pos = '.';
if (decimal_point_len > 1)
{
// move the fraction and exponent behind the '.' (std::copy
// allows the overlap, as the destination is before the source)
// NOLINTNEXTLINE(readability-qualified-auto,llvm-qualified-auto): std::copy returns an iterator, see https://github.com/nlohmann/json/issues/3081
const auto end = std::copy(dec_pos + static_cast<std::ptrdiff_t>(decimal_point_len), last, dec_pos + 1);
*end = '\0';
len = end - first;
}
} }
} }
+8
View File
@@ -397,6 +397,14 @@ TEST_CASE("locale with a multi-byte decimal point")
// a value the locale-independent paths convert is not affected // a value the locale-independent paths convert is not affected
CHECK(json::parse("12.5") == 12.5); CHECK(json::parse("12.5") == 12.5);
// dump() of a long double that takes the snprintf path replaces the
// whole decimal point, not only its first byte (#5993)
CHECK(long_double_json(1.5L).dump() == "1.5");
CHECK(long_double_json(2.0L).dump() == "2.0");
CHECK(long_double_json(-0.25L).dump() == "-0.25");
CHECK(long_double_json::parse(long_double_json(1.5e-7L).dump()) == long_double_json(1.5e-7L));
CHECK(long_double_json::parse(ld.dump()) == expected_long_double);
} }
if (!tested) if (!tested)
{ {