mirror of
https://github.com/nlohmann/json.git
synced 2026-09-07 00:37:58 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d027f06a42 |
@@ -34,8 +34,6 @@ Strong guarantee: if an exception is thrown, there are no changes in the JSON va
|
||||
("add", "remove", "move")
|
||||
- Throws [`out_of_range.411`](../../home/exceptions.md#jsonexceptionout_of_range411) if an "add" operation's target
|
||||
location has a parent that is neither an object nor an array.
|
||||
- Throws [`out_of_range.413`](../../home/exceptions.md#jsonexceptionout_of_range413) if a "remove" operation's target
|
||||
location has a parent that is neither an object nor an array.
|
||||
- Throws [`other_error.501`](../../home/exceptions.md#jsonexceptionother_error501) if "test" operation was
|
||||
unsuccessful.
|
||||
|
||||
@@ -77,5 +75,3 @@ is thrown. In any case, the original value is not changed: the patch is applied
|
||||
- Added in version 2.0.0.
|
||||
- Added [`out_of_range.411`](../../home/exceptions.md#jsonexceptionout_of_range411) and stopped relying on an internal assertion when an "add" operation's
|
||||
target location has a non-object/non-array parent in version 3.13.0.
|
||||
- Added [`out_of_range.413`](../../home/exceptions.md#jsonexceptionout_of_range413) and stopped silently ignoring a "remove" operation whose target
|
||||
location has a non-object/non-array parent in version 3.13.0.
|
||||
|
||||
@@ -30,8 +30,6 @@ No guarantees, value may be corrupted by an unsuccessful patch operation.
|
||||
("add", "remove", "move")
|
||||
- Throws [`out_of_range.411`](../../home/exceptions.md#jsonexceptionout_of_range411) if an "add" operation's target
|
||||
location has a parent that is neither an object nor an array.
|
||||
- Throws [`out_of_range.413`](../../home/exceptions.md#jsonexceptionout_of_range413) if a "remove" operation's target
|
||||
location has a parent that is neither an object nor an array.
|
||||
- Throws [`other_error.501`](../../home/exceptions.md#jsonexceptionother_error501) if "test" operation was
|
||||
unsuccessful.
|
||||
|
||||
@@ -74,5 +72,3 @@ function throws an exception.
|
||||
- Added in version 3.11.0.
|
||||
- Added [`out_of_range.411`](../../home/exceptions.md#jsonexceptionout_of_range411) and stopped relying on an internal assertion when an "add" operation's
|
||||
target location has a non-object/non-array parent in version 3.13.0.
|
||||
- Added [`out_of_range.413`](../../home/exceptions.md#jsonexceptionout_of_range413) and stopped silently ignoring a "remove" operation whose target
|
||||
location has a non-object/non-array parent in version 3.13.0.
|
||||
|
||||
@@ -933,20 +933,6 @@ BSON stores the length of documents, arrays, strings, and binary values in a sig
|
||||
[`to_bson`](../api/basic_json/to_bson.md) produced documents with negative length prefixes that
|
||||
[`from_bson`](../api/basic_json/from_bson.md) rejected.
|
||||
|
||||
### json.exception.out_of_range.413
|
||||
|
||||
A JSON Patch `remove` operation cannot be applied because the target location's parent is neither an object nor an array. Per [RFC 6902](https://datatracker.ietf.org/doc/html/rfc6902), a `remove` target must reference a member of an existing object or an element of an existing array; a primitive value (string, number, boolean, etc.) or `null` has no members or elements to remove.
|
||||
|
||||
!!! failure "Example message"
|
||||
|
||||
```
|
||||
cannot remove value: the JSON Patch 'remove' target's parent is of type number, but must be an object or array
|
||||
```
|
||||
|
||||
!!! note
|
||||
|
||||
This exception was added in version 3.13.0. Before that, this situation was silently ignored (the `remove` operation had no effect).
|
||||
|
||||
## Further exceptions
|
||||
|
||||
This exception is thrown in case of errors that cannot be classified with the
|
||||
|
||||
@@ -71,7 +71,7 @@ class serializer
|
||||
, thousands_sep(loc->thousands_sep == nullptr ? '\0' : std::char_traits<char>::to_char_type(* (loc->thousands_sep)))
|
||||
, decimal_point(loc->decimal_point == nullptr ? '\0' : std::char_traits<char>::to_char_type(* (loc->decimal_point)))
|
||||
, indent_char(ichar)
|
||||
, indent_string(512, indent_char)
|
||||
, indent_string()
|
||||
, error_handler(error_handler_)
|
||||
{}
|
||||
|
||||
@@ -126,6 +126,10 @@ class serializer
|
||||
|
||||
// variable to hold indentation for recursive calls
|
||||
const auto new_indent = current_indent + indent_step;
|
||||
if (JSON_HEDLEY_UNLIKELY(indent_string.empty()))
|
||||
{
|
||||
indent_string.resize(512, indent_char);
|
||||
}
|
||||
if (JSON_HEDLEY_UNLIKELY(indent_string.size() < new_indent))
|
||||
{
|
||||
indent_string.resize(indent_string.size() * 2, ' ');
|
||||
@@ -199,6 +203,10 @@ class serializer
|
||||
|
||||
// variable to hold indentation for recursive calls
|
||||
const auto new_indent = current_indent + indent_step;
|
||||
if (JSON_HEDLEY_UNLIKELY(indent_string.empty()))
|
||||
{
|
||||
indent_string.resize(512, indent_char);
|
||||
}
|
||||
if (JSON_HEDLEY_UNLIKELY(indent_string.size() < new_indent))
|
||||
{
|
||||
indent_string.resize(indent_string.size() * 2, ' ');
|
||||
@@ -260,6 +268,10 @@ class serializer
|
||||
|
||||
// variable to hold indentation for recursive calls
|
||||
const auto new_indent = current_indent + indent_step;
|
||||
if (JSON_HEDLEY_UNLIKELY(indent_string.empty()))
|
||||
{
|
||||
indent_string.resize(512, indent_char);
|
||||
}
|
||||
if (JSON_HEDLEY_UNLIKELY(indent_string.size() < new_indent))
|
||||
{
|
||||
indent_string.resize(indent_string.size() * 2, ' ');
|
||||
@@ -1010,7 +1022,7 @@ class serializer
|
||||
|
||||
/// the indentation character
|
||||
const char indent_char;
|
||||
/// the indentation string
|
||||
/// the indentation string (lazily allocated on first use by a pretty-print branch)
|
||||
string_t indent_string;
|
||||
|
||||
/// error_handler how to react on decoding errors
|
||||
|
||||
@@ -4939,12 +4939,6 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
|
||||
// note erase performs range check
|
||||
parent.erase(json_pointer::template array_index<basic_json_t>(last_path));
|
||||
}
|
||||
else
|
||||
{
|
||||
// the parent of a "remove" target must be an object or array
|
||||
// (see #5396)
|
||||
JSON_THROW(out_of_range::create(413, detail::concat("cannot remove value: the JSON Patch 'remove' target's parent is of type ", parent.type_name(), ", but must be an object or array"), &parent));
|
||||
}
|
||||
};
|
||||
|
||||
// type check: top level value must be an array
|
||||
|
||||
@@ -20150,7 +20150,7 @@ class serializer
|
||||
, thousands_sep(loc->thousands_sep == nullptr ? '\0' : std::char_traits<char>::to_char_type(* (loc->thousands_sep)))
|
||||
, decimal_point(loc->decimal_point == nullptr ? '\0' : std::char_traits<char>::to_char_type(* (loc->decimal_point)))
|
||||
, indent_char(ichar)
|
||||
, indent_string(512, indent_char)
|
||||
, indent_string()
|
||||
, error_handler(error_handler_)
|
||||
{}
|
||||
|
||||
@@ -20205,6 +20205,10 @@ class serializer
|
||||
|
||||
// variable to hold indentation for recursive calls
|
||||
const auto new_indent = current_indent + indent_step;
|
||||
if (JSON_HEDLEY_UNLIKELY(indent_string.empty()))
|
||||
{
|
||||
indent_string.resize(512, indent_char);
|
||||
}
|
||||
if (JSON_HEDLEY_UNLIKELY(indent_string.size() < new_indent))
|
||||
{
|
||||
indent_string.resize(indent_string.size() * 2, ' ');
|
||||
@@ -20278,6 +20282,10 @@ class serializer
|
||||
|
||||
// variable to hold indentation for recursive calls
|
||||
const auto new_indent = current_indent + indent_step;
|
||||
if (JSON_HEDLEY_UNLIKELY(indent_string.empty()))
|
||||
{
|
||||
indent_string.resize(512, indent_char);
|
||||
}
|
||||
if (JSON_HEDLEY_UNLIKELY(indent_string.size() < new_indent))
|
||||
{
|
||||
indent_string.resize(indent_string.size() * 2, ' ');
|
||||
@@ -20339,6 +20347,10 @@ class serializer
|
||||
|
||||
// variable to hold indentation for recursive calls
|
||||
const auto new_indent = current_indent + indent_step;
|
||||
if (JSON_HEDLEY_UNLIKELY(indent_string.empty()))
|
||||
{
|
||||
indent_string.resize(512, indent_char);
|
||||
}
|
||||
if (JSON_HEDLEY_UNLIKELY(indent_string.size() < new_indent))
|
||||
{
|
||||
indent_string.resize(indent_string.size() * 2, ' ');
|
||||
@@ -21089,7 +21101,7 @@ class serializer
|
||||
|
||||
/// the indentation character
|
||||
const char indent_char;
|
||||
/// the indentation string
|
||||
/// the indentation string (lazily allocated on first use by a pretty-print branch)
|
||||
string_t indent_string;
|
||||
|
||||
/// error_handler how to react on decoding errors
|
||||
@@ -26367,12 +26379,6 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
|
||||
// note erase performs range check
|
||||
parent.erase(json_pointer::template array_index<basic_json_t>(last_path));
|
||||
}
|
||||
else
|
||||
{
|
||||
// the parent of a "remove" target must be an object or array
|
||||
// (see #5396)
|
||||
JSON_THROW(out_of_range::create(413, detail::concat("cannot remove value: the JSON Patch 'remove' target's parent is of type ", parent.type_name(), ", but must be an object or array"), &parent));
|
||||
}
|
||||
};
|
||||
|
||||
// type check: top level value must be an array
|
||||
|
||||
@@ -1389,61 +1389,6 @@ TEST_CASE("JSON patch - add to a primitive parent (regression #4292)")
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("JSON patch - remove with primitive or null parent (regression #5396)")
|
||||
{
|
||||
// Regression test for https://github.com/nlohmann/json/issues/5396
|
||||
//
|
||||
// RFC 6902 (§4.2) requires the target location of a "remove" operation
|
||||
// to exist. When the target's parent resolves to a primitive value or
|
||||
// null, the operation must fail. Previously operation_remove silently
|
||||
// did nothing in this case (neither the "is_object" nor the "is_array"
|
||||
// branch matched, and there was no final "else"), so the patch appeared
|
||||
// to succeed without changing the document. It now throws
|
||||
// out_of_range.413.
|
||||
|
||||
SECTION("parent is a primitive (number)")
|
||||
{
|
||||
json const doc = {{"a", 1}};
|
||||
json const patch = {{{"op", "remove"}, {"path", "/a/b"}}};
|
||||
#if JSON_DIAGNOSTICS
|
||||
CHECK_THROWS_WITH_AS(doc.patch(patch), "[json.exception.out_of_range.413] (/a) cannot remove value: the JSON Patch 'remove' target's parent is of type number, but must be an object or array", json::out_of_range&);
|
||||
#else
|
||||
CHECK_THROWS_WITH_AS(doc.patch(patch), "[json.exception.out_of_range.413] cannot remove value: the JSON Patch 'remove' target's parent is of type number, but must be an object or array", json::out_of_range&);
|
||||
#endif
|
||||
}
|
||||
|
||||
SECTION("parent is a primitive (string)")
|
||||
{
|
||||
json const doc = {{"foo", {{"bar", "a string"}}}};
|
||||
json const patch = {{{"op", "remove"}, {"path", "/foo/bar/baz"}}};
|
||||
#if JSON_DIAGNOSTICS
|
||||
CHECK_THROWS_WITH_AS(doc.patch(patch), "[json.exception.out_of_range.413] (/foo/bar) cannot remove value: the JSON Patch 'remove' target's parent is of type string, but must be an object or array", json::out_of_range&);
|
||||
#else
|
||||
CHECK_THROWS_WITH_AS(doc.patch(patch), "[json.exception.out_of_range.413] cannot remove value: the JSON Patch 'remove' target's parent is of type string, but must be an object or array", json::out_of_range&);
|
||||
#endif
|
||||
}
|
||||
|
||||
SECTION("top-level document is null")
|
||||
{
|
||||
json const doc = nullptr;
|
||||
json const patch = {{{"op", "remove"}, {"path", "/a"}}};
|
||||
CHECK_THROWS_WITH_AS(doc.patch(patch), "[json.exception.out_of_range.413] cannot remove value: the JSON Patch 'remove' target's parent is of type null, but must be an object or array", json::out_of_range&);
|
||||
}
|
||||
|
||||
SECTION("legitimate removes still work")
|
||||
{
|
||||
// object member
|
||||
json const doc1 = {{"a", 1}, {"b", 2}};
|
||||
json const patch1 = {{{"op", "remove"}, {"path", "/a"}}};
|
||||
CHECK(doc1.patch(patch1) == json({{"b", 2}}));
|
||||
|
||||
// array element
|
||||
json const doc2 = R"([1, 2, 3])"_json;
|
||||
json const patch2 = {{{"op", "remove"}, {"path", "/1"}}};
|
||||
CHECK(doc2.patch(patch2) == R"([1, 3])"_json);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("JSON patch - diff emits array removals in descending index order")
|
||||
{
|
||||
SECTION("array shrunk to empty")
|
||||
|
||||
@@ -14,6 +14,40 @@ using nlohmann::json;
|
||||
#include <array>
|
||||
#include <sstream>
|
||||
#include <iomanip>
|
||||
#include <cstdlib>
|
||||
#include <new>
|
||||
|
||||
namespace
|
||||
{
|
||||
// heap allocation counter used by the regression test for issue #5413
|
||||
// (https://github.com/nlohmann/json/issues/5413); disabled (and thus a
|
||||
// no-op besides the counting) unless explicitly toggled on
|
||||
bool count_heap_allocations = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
std::size_t heap_allocations = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
} // namespace
|
||||
|
||||
void* operator new (std::size_t size) // NOLINT(cppcoreguidelines-owning-memory,misc-new-delete-overloads)
|
||||
{
|
||||
if (count_heap_allocations)
|
||||
{
|
||||
++heap_allocations;
|
||||
}
|
||||
if (void* ptr = std::malloc(size)) // NOLINT(cppcoreguidelines-no-malloc,cppcoreguidelines-owning-memory)
|
||||
{
|
||||
return ptr;
|
||||
}
|
||||
throw std::bad_alloc(); // NOLINT(hicpp-exception-baseclass)
|
||||
}
|
||||
|
||||
void operator delete (void* ptr) noexcept // NOLINT(cppcoreguidelines-owning-memory,misc-new-delete-overloads)
|
||||
{
|
||||
std::free(ptr); // NOLINT(cppcoreguidelines-no-malloc,cppcoreguidelines-owning-memory)
|
||||
}
|
||||
|
||||
void operator delete (void* ptr, std::size_t /*size*/) noexcept // NOLINT(cppcoreguidelines-owning-memory,misc-new-delete-overloads)
|
||||
{
|
||||
std::free(ptr); // NOLINT(cppcoreguidelines-no-malloc,cppcoreguidelines-owning-memory)
|
||||
}
|
||||
|
||||
TEST_CASE("serialization")
|
||||
{
|
||||
@@ -382,3 +416,76 @@ TEST_CASE("dump for basic_json with long double number_float_t")
|
||||
check_same(100.0L, 100.0);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("regression test for issue #5413 - lazily allocated indent_string")
|
||||
{
|
||||
// the serializer used to unconditionally allocate a 512-byte
|
||||
// indent_string in its constructor, even though it is only ever read
|
||||
// inside the pretty_print branches of dump(). This wasted a heap
|
||||
// allocation (and its matching deallocation) on every single compact
|
||||
// (i.e. non-pretty, the default) dump() call. indent_string is now
|
||||
// allocated lazily, the first time a pretty-print branch actually
|
||||
// needs it -- so a compact dump() must perform strictly fewer heap
|
||||
// allocations than a pretty dump() of the same value.
|
||||
const json j = {{"level", "info"}, {"msg", "hello world"}, {"id", 12345}};
|
||||
|
||||
// warm up anything unrelated to indentation (e.g., one-time locale
|
||||
// lookups) that might otherwise allocate on first use regardless of
|
||||
// pretty-printing, so it does not skew the counts measured below
|
||||
const auto warmup = j.dump();
|
||||
const auto warmup_pretty = j.dump(4);
|
||||
CHECK(!warmup.empty());
|
||||
CHECK(!warmup_pretty.empty());
|
||||
|
||||
SECTION("compact dump() has a stable, minimal allocation count")
|
||||
{
|
||||
count_heap_allocations = true;
|
||||
|
||||
heap_allocations = 0;
|
||||
const auto compact1 = j.dump();
|
||||
const auto allocs_compact1 = heap_allocations;
|
||||
|
||||
heap_allocations = 0;
|
||||
const auto compact2 = j.dump(-1);
|
||||
const auto allocs_compact2 = heap_allocations;
|
||||
|
||||
count_heap_allocations = false;
|
||||
|
||||
CHECK(compact1 == compact2);
|
||||
// dump() and dump(-1) both take the compact code path and must
|
||||
// never touch indent_string, so they allocate identically often
|
||||
CHECK(allocs_compact1 == allocs_compact2);
|
||||
}
|
||||
|
||||
SECTION("first pretty dump() allocates more than a compact dump()")
|
||||
{
|
||||
// use a tiny value whose compact ({"a":1}, 7 bytes) and pretty
|
||||
// ({"a": 1} with 1-space indent, 11 bytes) serializations both stay
|
||||
// well inside every common std::string small-string-optimization
|
||||
// buffer (>= 15 bytes on libstdc++/MSVC STL, >= 22 on libc++), so
|
||||
// building the result string itself causes no heap allocation
|
||||
// either way -- isolating indent_string as the only thing that can
|
||||
// possibly account for a difference in allocation count
|
||||
const json tiny = {{"a", 1}};
|
||||
|
||||
count_heap_allocations = true;
|
||||
|
||||
heap_allocations = 0;
|
||||
const auto compact = tiny.dump();
|
||||
const auto allocs_compact = heap_allocations;
|
||||
|
||||
heap_allocations = 0;
|
||||
const auto pretty = tiny.dump(1);
|
||||
const auto allocs_pretty = heap_allocations;
|
||||
|
||||
count_heap_allocations = false;
|
||||
|
||||
CHECK(compact == "{\"a\":1}");
|
||||
CHECK(pretty == "{\n \"a\": 1\n}");
|
||||
// a fresh serializer is created per dump() call; the pretty branch
|
||||
// lazily allocates indent_string on its first use, so it must
|
||||
// allocate at least once more than the compact branch, which never
|
||||
// touches indent_string at all
|
||||
CHECK(allocs_pretty > allocs_compact);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user