Compare commits

..
Author SHA1 Message Date
Niels Lohmann 2241161307 insert(): strong guarantee for single elements, basic for the rest
Rework of the previous two commits, following the discussion in #5996:
the standard containers give the strong guarantee only for a single
element (std::vector: at the end, or if the exception does not come from
copying T; std::map: single-element insert), and copying a basic_json
allocates. Rebuilding a rollback for the count, range and object-range
overloads on top of array_t/object_t made the code verbose and slower.

- insert(pos, const basic_json&): at end() forward as before (the
  standard covers it); elsewhere copy first and insert by move, as
  libstdc++ does internally (libc++ shifts the tail before copying).
- insert_iterator() and object insert(first, last) set the parents in a
  scope guard, so a failed insertion leaves a valid value with
  JSON_DIAGNOSTICS too (the next assert_invariant() aborted before).
- Docs: strong guarantee for (1), basic guarantee for (2)-(5); the
  argument checks throw before anything is changed.
- Test: allocation-failure sweep checks the strong guarantee for (1) and
  validity, type and parents for the other overloads.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-11-02 08:54:34 +01:00
Claude 727a15c710 Fix clang-tidy and -Wnrvo findings in insert rollback helpers
Declare the rollback helpers non-movable, replace the nested conditional
and C array in object_insert_rollback, silence the intentional copy in the
allocator test, and avoid the missed NRVO in the test factory lambda.

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:44:29 +00:00
Niels Lohmann c84e22cc62 Make basic_json::insert() provide its documented strong exception guarantee (#5986)
A failed allocation while the new elements were copied left the value
changed, and with JSON_DIAGNOSTICS stale parent pointers behind. The
extra work now happens almost only on the failure path:

- Array insert(pos, cnt, val) and insert(pos, first, last) append the
  copies at the end and rotate them into place. If a copy throws, the
  appended elements are erased again (noexcept) and the parents are set.
  Moving a basic_json cannot throw, so reallocation is safe. This is no
  slower than vector::insert (faster for numbers), needs no temporary
  array, and keeps the elements that libstdc++ lost on reallocation.
- Array insert(pos, val) copies a non-scalar value first and inserts it
  by move, except at the end, where vector::insert is strong. Scalars
  keep the direct path.
- The object range insert(first, last) into an empty object uses the
  container's own range insertion and clears the object on an exception.
  Into a non-empty object it inserts member by member and logs one bit
  per source member (a new member is detected by the size change; no
  allocation for the first 256 members); on an exception the members
  added are erased by walking the source range. No assumption that
  key_type is string_t.
- The parents are set when a failed insertion is undone.

The regression test fails every allocation of every insert() overload in
turn, for json and ordered_json, and checks the value and its parents.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-11-01 22:27:43 +01:00
Niels Lohmann 67b103e506 Fix clang-tidy errors in json_view tests on develop (#5989)
readability-math-missing-parentheses in unit-json_view_builder.cpp (#5974)
and performance-inefficient-string-concatenation in
unit-json_view_positions.cpp.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-11-01 20:42:58 +01:00
11 changed files with 358 additions and 118 deletions

No files matched your search

+11 -1
View File
@@ -65,7 +65,17 @@ the same index, which is now a different value.
## Exception safety
Strong exception safety: if an exception occurs, the original value stays intact.
1. Strong guarantee: if an exception is thrown, there are no changes in the JSON value.
2. Basic guarantee: if an exception is thrown while the elements are copied (for example, `std::bad_alloc`), the JSON
value may be partially modified, but it stays a valid array.
3. Basic guarantee: if an exception is thrown while the elements are copied (for example, `std::bad_alloc`), the JSON
value may be partially modified, but it stays a valid array.
4. Basic guarantee: if an exception is thrown while the elements are copied (for example, `std::bad_alloc`), the JSON
value may be partially modified, but it stays a valid array.
5. Basic guarantee: if an exception is thrown while the members are copied (for example, `std::bad_alloc`), the JSON
value may be partially modified, but it stays a valid object.
The exceptions listed below are thrown before the JSON value is changed.
## Exceptions
@@ -35,9 +35,6 @@ Note that enabling this macro increases the size of every JSON value by two `std
overhead to parsing, copying JSON value objects, and the generation of error messages for exceptions. It also causes
these values to be reported in those error messages.
If [`JSON_DIAGNOSTICS`](json_diagnostics.md) is also set, the message contains the path and the byte range, such as
`(/1) (bytes 4-8)`. An exception about the root value has no path and only the range, such as `(bytes 0-4)`.
## Default definition
The default value is `0` (position diagnostics are switched off).
+10 -12
View File
@@ -198,19 +198,18 @@ class exception : public std::exception
// all other parent types (primitives) contribute no token
}
// the root value has no path, but still has a byte range
std::string prefix;
if (!tokens.empty())
if (tokens.empty())
{
const auto str = std::accumulate(tokens.rbegin(), tokens.rend(), std::string{},
[](const std::string & a, const std::string & b)
{
return concat(a, '/', detail::escape(b));
});
prefix = concat('(', str, ") ");
return "";
}
return concat(prefix, get_byte_positions(leaf_element));
auto str = std::accumulate(tokens.rbegin(), tokens.rend(), std::string{},
[](const std::string & a, const std::string & b)
{
return concat(a, '/', detail::escape(b));
});
return concat('(', str, ") ", get_byte_positions(leaf_element));
#else
return get_byte_positions(leaf_element);
#endif
@@ -223,8 +222,7 @@ class exception : public std::exception
template<typename BasicJsonType>
static std::string get_byte_positions(const BasicJsonType* leaf_element)
{
// a value-initialized iterator has no value (invalid_iterator.212)
if (leaf_element != nullptr && leaf_element->start_pos() != std::string::npos && leaf_element->end_pos() != std::string::npos)
if ((leaf_element->start_pos() != std::string::npos) && (leaf_element->end_pos() != std::string::npos))
{
return concat("(bytes ", std::to_string(leaf_element->start_pos()), "-", std::to_string(leaf_element->end_pos()), ") ");
}
+13 -12
View File
@@ -234,7 +234,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
/// 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 (with JSON_DIAGNOSTICS, only
/// if there is a path)
///
/// 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
@@ -255,19 +256,19 @@ std::string diagnostics(const error_site site)
std::string context;
#if JSON_DIAGNOSTICS
std::vector<std::string> tokens;
// a value that is not found leaves no tokens, like the root
static_cast<void>(edited ? value_path<true>(d, site.value(), tokens) : value_path<false>(d, site.value(), tokens));
if (!tokens.empty())
const bool found = edited ? value_path<true>(d, site.value(), tokens) : value_path<false>(d, site.value(), tokens);
if (!found || tokens.empty())
{
// the root value has no path, but still has a byte range
std::string path;
for (const std::string& token : tokens)
{
path.push_back('/');
path += detail::escape(token);
}
context = concat('(', path, ") ");
// context is still empty; returning it keeps the copy elided (-Wnrvo)
return context;
}
std::string path;
for (const std::string& token : tokens)
{
path.push_back('/');
path += detail::escape(token);
}
context = concat('(', path, ") ");
#endif
#if JSON_DIAGNOSTIC_POSITIONS
std::size_t start = 0;
+36 -4
View File
@@ -4914,6 +4914,30 @@ public:
return {it, res.second};
}
private:
/// Sets the parents of a value when it goes out of scope, also when an
/// exception is thrown: a container insertion that throws may already
/// have added elements, whose parents must be set for the value to stay
/// valid (basic exception guarantee). Does nothing without JSON_DIAGNOSTICS.
class set_parents_on_exit
{
public:
explicit set_parents_on_exit(basic_json* v) noexcept : m_value(v) {}
set_parents_on_exit(const set_parents_on_exit&) = delete;
set_parents_on_exit& operator=(const set_parents_on_exit&) = delete;
set_parents_on_exit(set_parents_on_exit&&) = delete;
set_parents_on_exit& operator=(set_parents_on_exit&&) = delete;
~set_parents_on_exit()
{
m_value->set_parents();
}
private:
basic_json* m_value;
};
public:
/// Helper for insertion of an iterator
/// @note: This uses std::distance to support GCC 4.8,
/// see https://github.com/nlohmann/json/pull/1257
@@ -4923,6 +4947,7 @@ public:
iterator result(this);
JSON_ASSERT(m_data.m_value.array != nullptr);
const set_parents_on_exit parents(this);
auto insert_pos = std::distance(m_data.m_value.array->begin(), pos.m_it.array_iterator);
m_data.m_value.array->insert(pos.m_it.array_iterator, std::forward<Args>(args)...);
result.m_it.array_iterator = m_data.m_value.array->begin() + insert_pos;
@@ -4931,7 +4956,6 @@ public:
// result.m_it.array_iterator = m_data.m_value.array->insert(pos.m_it.array_iterator, cnt, val);
// but the return value of insert is missing in GCC 4.8, so it is written this way instead.
set_parents();
return result;
}
@@ -4951,8 +4975,16 @@ public:
throw_iterator_from_other_value(this);
}
// insert to array and return iterator
return insert_iterator(pos, val);
// vector::insert has no effects if copying a single element to the
// end throws; elsewhere, it may already have shifted the elements, so
// copy first and insert by move, which cannot throw but by allocating
if (pos.m_it.array_iterator == m_data.m_value.array->end())
{
return insert_iterator(pos, val);
}
basic_json tmp(val);
return insert_iterator(pos, std::move(tmp));
}
/// @brief inserts element into array
@@ -5084,8 +5116,8 @@ public:
detail::throw_literal<invalid_iterator>(detail::exception_id::iterator_from_other_value, "iterators first and last must point to objects", this);
}
const set_parents_on_exit parents(this);
m_data.m_value.object->insert(first.m_it.object_iterator, last.m_it.object_iterator);
set_parents();
}
/// @brief updates a JSON object from another object, overwriting existing keys
+46 -16
View File
@@ -5695,19 +5695,18 @@ class exception : public std::exception
// all other parent types (primitives) contribute no token
}
// the root value has no path, but still has a byte range
std::string prefix;
if (!tokens.empty())
if (tokens.empty())
{
const auto str = std::accumulate(tokens.rbegin(), tokens.rend(), std::string{},
[](const std::string & a, const std::string & b)
{
return concat(a, '/', detail::escape(b));
});
prefix = concat('(', str, ") ");
return "";
}
return concat(prefix, get_byte_positions(leaf_element));
auto str = std::accumulate(tokens.rbegin(), tokens.rend(), std::string{},
[](const std::string & a, const std::string & b)
{
return concat(a, '/', detail::escape(b));
});
return concat('(', str, ") ", get_byte_positions(leaf_element));
#else
return get_byte_positions(leaf_element);
#endif
@@ -5720,8 +5719,7 @@ class exception : public std::exception
template<typename BasicJsonType>
static std::string get_byte_positions(const BasicJsonType* leaf_element)
{
// a value-initialized iterator has no value (invalid_iterator.212)
if (leaf_element != nullptr && leaf_element->start_pos() != std::string::npos && leaf_element->end_pos() != std::string::npos)
if ((leaf_element->start_pos() != std::string::npos) && (leaf_element->end_pos() != std::string::npos))
{
return concat("(bytes ", std::to_string(leaf_element->start_pos()), "-", std::to_string(leaf_element->end_pos()), ") ");
}
@@ -42708,6 +42706,30 @@ public:
return {it, res.second};
}
private:
/// Sets the parents of a value when it goes out of scope, also when an
/// exception is thrown: a container insertion that throws may already
/// have added elements, whose parents must be set for the value to stay
/// valid (basic exception guarantee). Does nothing without JSON_DIAGNOSTICS.
class set_parents_on_exit
{
public:
explicit set_parents_on_exit(basic_json* v) noexcept : m_value(v) {}
set_parents_on_exit(const set_parents_on_exit&) = delete;
set_parents_on_exit& operator=(const set_parents_on_exit&) = delete;
set_parents_on_exit(set_parents_on_exit&&) = delete;
set_parents_on_exit& operator=(set_parents_on_exit&&) = delete;
~set_parents_on_exit()
{
m_value->set_parents();
}
private:
basic_json* m_value;
};
public:
/// Helper for insertion of an iterator
/// @note: This uses std::distance to support GCC 4.8,
/// see https://github.com/nlohmann/json/pull/1257
@@ -42717,6 +42739,7 @@ public:
iterator result(this);
JSON_ASSERT(m_data.m_value.array != nullptr);
const set_parents_on_exit parents(this);
auto insert_pos = std::distance(m_data.m_value.array->begin(), pos.m_it.array_iterator);
m_data.m_value.array->insert(pos.m_it.array_iterator, std::forward<Args>(args)...);
result.m_it.array_iterator = m_data.m_value.array->begin() + insert_pos;
@@ -42725,7 +42748,6 @@ public:
// result.m_it.array_iterator = m_data.m_value.array->insert(pos.m_it.array_iterator, cnt, val);
// but the return value of insert is missing in GCC 4.8, so it is written this way instead.
set_parents();
return result;
}
@@ -42745,8 +42767,16 @@ public:
throw_iterator_from_other_value(this);
}
// insert to array and return iterator
return insert_iterator(pos, val);
// vector::insert has no effects if copying a single element to the
// end throws; elsewhere, it may already have shifted the elements, so
// copy first and insert by move, which cannot throw but by allocating
if (pos.m_it.array_iterator == m_data.m_value.array->end())
{
return insert_iterator(pos, val);
}
basic_json tmp(val);
return insert_iterator(pos, std::move(tmp));
}
/// @brief inserts element into array
@@ -42878,8 +42908,8 @@ public:
detail::throw_literal<invalid_iterator>(detail::exception_id::iterator_from_other_value, "iterators first and last must point to objects", this);
}
const set_parents_on_exit parents(this);
m_data.m_value.object->insert(first.m_it.object_iterator, last.m_it.object_iterator);
set_parents();
}
/// @brief updates a JSON object from another object, overwriting existing keys
+13 -12
View File
@@ -5076,7 +5076,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
/// 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 (with JSON_DIAGNOSTICS, only
/// if there is a path)
///
/// 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
@@ -5097,19 +5098,19 @@ std::string diagnostics(const error_site site)
std::string context;
#if JSON_DIAGNOSTICS
std::vector<std::string> tokens;
// a value that is not found leaves no tokens, like the root
static_cast<void>(edited ? value_path<true>(d, site.value(), tokens) : value_path<false>(d, site.value(), tokens));
if (!tokens.empty())
const bool found = edited ? value_path<true>(d, site.value(), tokens) : value_path<false>(d, site.value(), tokens);
if (!found || tokens.empty())
{
// the root value has no path, but still has a byte range
std::string path;
for (const std::string& token : tokens)
{
path.push_back('/');
path += detail::escape(token);
}
context = concat('(', path, ") ");
// context is still empty; returning it keeps the copy elided (-Wnrvo)
return context;
}
std::string path;
for (const std::string& token : tokens)
{
path.push_back('/');
path += detail::escape(token);
}
context = concat('(', path, ") ");
#endif
#if JSON_DIAGNOSTIC_POSITIONS
std::size_t start = 0;
+220
View File
@@ -12,6 +12,7 @@
#include <nlohmann/json.hpp>
using nlohmann::json;
#include <functional>
#include <valarray>
#if JSON_HAS_RANGES
#include <ranges>
@@ -804,3 +805,222 @@ TEST_CASE("a failed allocation leaves the value unchanged")
#endif
}
#endif
// the no-exceptions CI job has no std::bad_alloc to catch; with iterator
// debugging (MSVC STL debug builds) containers allocate inside noexcept
// functions, so failing an allocation there terminates the program
#if !defined(JSON_NOEXCEPTION) && !(defined(_ITERATOR_DEBUG_LEVEL) && _ITERATOR_DEBUG_LEVEL > 0)
namespace
{
// whether every child of j names j as its parent (always true without
// JSON_DIAGNOSTICS, where no parents are stored)
template<class BasicJsonType>
bool children_name_their_parent(const BasicJsonType& j)
{
#if JSON_DIAGNOSTICS
if (j.is_array())
{
for (const auto& element : *j.m_data.m_value.array)
{
if (element.m_parent != &j)
{
return false;
}
}
}
else if (j.is_object())
{
for (const auto& element : *j.m_data.m_value.object)
{
if (element.second.m_parent != &j)
{
return false;
}
}
}
#else
static_cast<void>(j);
#endif
return true;
}
enum class guarantee
{
basic, // the value stays valid and keeps its type
strong // the value is unchanged
};
// Runs op on a fresh value make() with the 0th, 1st, 2nd, ... allocation made
// by the library failing in turn, until op completes. Every failed attempt
// must have thrown std::bad_alloc and left the value valid, with all parent
// pointers intact (copying a value asserts the invariant), and for the strong
// guarantee exactly as it was.
template<class BasicJsonType>
void check_exception_guarantee(guarantee g,
const std::function<BasicJsonType()>& make,
const std::function<void(BasicJsonType&)>& op)
{
fail_at_alloc_call = -1;
bool completed = false;
std::size_t failures = 0;
for (std::size_t n = 0; !completed; ++n)
{
CAPTURE(n)
REQUIRE(n < 1000);
BasicJsonType j = make();
const std::string before = j.dump();
const auto type_before = j.type();
alloc_call_count = 0;
fail_at_alloc_call = static_cast<long>(n);
try
{
op(j);
completed = true;
}
catch (const std::bad_alloc&)
{
++failures;
}
fail_at_alloc_call = -1;
if (!completed)
{
CHECK((j.type() == type_before));
CHECK(children_name_their_parent(j));
const BasicJsonType copy(j); // NOLINT(performance-unnecessary-copy-initialization): the copy is the point, it allocates
CHECK(copy.dump() == j.dump());
if (g == guarantee::strong)
{
CHECK(j.dump() == before);
}
}
}
// the operation allocates, so at least one attempt must have failed
CHECK(failures > 0);
}
template<class BasicJsonType>
void check_insert_exception_guarantees()
{
using json_t = BasicJsonType;
using make_t = std::function<json_t()>;
// strings of this length do not fit into the small string buffer, so
// copying them allocates
const json_t val = std::string(64, 'x');
const json_t source = json_t::array({std::string(64, 'p'), std::string(64, 'q'), std::string(64, 'r')});
for (const bool spare_capacity :
{
false, true
})
{
CAPTURE(spare_capacity)
// [ "aaa...", "bbb...", "ccc..." ], with a spare slot if requested
const make_t make = [spare_capacity]()
{
json_t j = json_t::array({std::string(64, 'a'), std::string(64, 'b'), std::string(64, 'c')});
if (spare_capacity)
{
j.push_back(std::string(64, 'd'));
j.erase(3);
}
return json_t(std::move(j));
};
for (const bool at_end :
{
false, true
})
{
CAPTURE(at_end)
SECTION("insert(pos, const basic_json&)")
{
check_exception_guarantee<json_t>(guarantee::strong, make, [&](json_t& j)
{
j.insert(at_end ? j.end() : j.begin() + 1, val);
});
}
SECTION("insert(pos, basic_json&&)")
{
check_exception_guarantee<json_t>(guarantee::strong, make, [&](json_t& j)
{
j.insert(at_end ? j.end() : j.begin() + 1, json_t(val));
});
}
SECTION("insert(pos, size_type, const basic_json&)")
{
check_exception_guarantee<json_t>(guarantee::basic, make, [&](json_t& j)
{
j.insert(at_end ? j.end() : j.begin() + 1, 3, val);
});
}
SECTION("insert(pos, const_iterator, const_iterator)")
{
check_exception_guarantee<json_t>(guarantee::basic, make, [&](json_t& j)
{
j.insert(at_end ? j.end() : j.begin() + 1, source.begin(), source.end());
});
}
SECTION("insert(pos, initializer_list_t)")
{
check_exception_guarantee<json_t>(guarantee::basic, make, [&](json_t& j)
{
j.insert(at_end ? j.end() : j.begin() + 1, {val, val});
});
}
}
}
SECTION("insert(const_iterator, const_iterator) for objects")
{
const json_t members = {{"m", std::string(64, 'm')}, {"n", std::string(64, 'n')}, {"o", std::string(64, 'o')}};
SECTION("no member exists yet")
{
check_exception_guarantee<json_t>(guarantee::basic, []()
{
return json_t({{"a", 1}});
}, [&](json_t& j)
{
j.insert(members.begin(), members.end());
});
}
SECTION("some members exist already")
{
check_exception_guarantee<json_t>(guarantee::basic, []()
{
return json_t({{"a", 1}, {"n", std::string(64, 'x')}});
}, [&](json_t& j)
{
j.insert(members.begin(), members.end());
});
}
}
}
} // namespace
TEST_CASE("insert() provides its documented exception guarantees (#5986)")
{
SECTION("std::map-backed object_t")
{
check_insert_exception_guarantees<nlohmann::json::with_allocator_t<nth_alloc_fails_allocator>>();
}
SECTION("ordered_map-backed object_t")
{
check_insert_exception_guarantees<nlohmann::ordered_json::with_allocator_t<nth_alloc_fails_allocator>>();
}
}
#endif
-33
View File
@@ -193,28 +193,6 @@ TEST_CASE("Better diagnostics with positions")
check_conversion(300);
}
SECTION("exception about the root value (#5987)")
{
// the root has no path, but still has a byte range
const json root = json::parse("true");
const json nested = json::parse("[1, true]");
const json constructed = true;
#if JSON_DIAGNOSTICS
CHECK_THROWS_WITH_AS(root.get<std::string>(),
"[json.exception.type_error.302] (bytes 0-4) type must be string, but is boolean", json::type_error);
CHECK_THROWS_WITH_AS(nested[1].get<std::string>(),
"[json.exception.type_error.302] (/1) (bytes 4-8) type must be string, but is boolean", json::type_error);
#else
CHECK_THROWS_WITH_AS(root.get<std::string>(),
"[json.exception.type_error.302] (bytes 0-4) type must be string, but is boolean", json::type_error);
CHECK_THROWS_WITH_AS(nested[1].get<std::string>(),
"[json.exception.type_error.302] (bytes 4-8) type must be string, but is boolean", json::type_error);
#endif
// a value without positions gets no range and no stray whitespace
CHECK_THROWS_WITH_AS(constructed.get<std::string>(),
"[json.exception.type_error.302] type must be string, but is boolean", json::type_error);
}
SECTION("JSON patch add to primitive parent (#4292)")
{
// the JSON Patch "add" target /foo/bar/baz has a string parent
@@ -265,14 +243,3 @@ TEST_CASE("values read from a binary format have no positions")
check_no_positions(j);
}
}
TEST_CASE("comparing a value-initialized iterator throws invalid_iterator.212")
{
// such an iterator has no value to take the byte positions from; the
// comparison used to dereference the null pointer instead of throwing
json j = {1, 2};
json::iterator it{};
CHECK_THROWS_WITH_AS(it == j.begin(), "[json.exception.invalid_iterator.212] cannot compare iterators of different containers", json::invalid_iterator);
CHECK_THROWS_WITH_AS(it < j.begin(), "[json.exception.invalid_iterator.212] cannot compare iterators of different containers", json::invalid_iterator);
CHECK_THROWS_WITH_AS(j.begin() == it, "[json.exception.invalid_iterator.212] cannot compare iterators of different containers", json::invalid_iterator);
}
+1 -1
View File
@@ -692,7 +692,7 @@ TEST_CASE("json_view builder: fraction digits")
check_same(number);
check_same("[" + number + "]");
std::string pair;
pair.reserve(number.size() * 2 + 3);
pair.reserve((number.size() * 2) + 3);
pair += '[';
pair += number;
pair += ',';
+8 -24
View File
@@ -300,27 +300,6 @@ TEST_CASE("json_view materialize() source positions")
static_cast<void>(j.at("list").at(1).get<int>());
}));
}
SECTION("exception about the root value (#5987)")
{
const json_document root = json_document::parse("true");
const json_document nested = json_document::parse("[1, true]");
CHECK(json_view_test::exception_of_call([&]
{
static_cast<void>(root.root().get<std::string>());
}) == "[json.exception.type_error.302] (bytes 0-4) type must be string, but is boolean");
#if JSON_DIAGNOSTICS
CHECK(json_view_test::exception_of_call([&]
{
static_cast<void>(nested.root()[1].get<std::string>());
}) == "[json.exception.type_error.302] (/1) (bytes 4-8) type must be string, but is boolean");
#else
CHECK(json_view_test::exception_of_call([&]
{
static_cast<void>(nested.root()[1].get<std::string>());
}) == "[json.exception.type_error.302] (bytes 4-8) type must be string, but is boolean");
#endif
}
#endif
SECTION("edited documents")
@@ -542,7 +521,8 @@ TEST_CASE("json_view exceptions name the path and the bytes of the value")
std::string text = "7";
for (int level = 0; level < 40; ++level)
{
text = R"([[1, [2]], )" + text + R"(, {"k": [3, {"l": [4]}]}, [5, [6, [7]]]])";
text.insert(0, R"([[1, [2]], )");
text += R"(, {"k": [3, {"l": [4]}]}, [5, [6, [7]]]])";
}
CAPTURE(text)
const json j = json::parse(text);
@@ -571,8 +551,12 @@ TEST_CASE("json_view exceptions name the path and the bytes of the value")
expected_zz += "(bytes 12-35) key 'zz' not found";
CHECK(exception_of_call([&] { static_cast<void>(d.root().at(json::json_pointer("/people/0/zz"))); }) == expected_zz);
// the root has no path, but still its bytes (#5987)
const std::string expected_missing = "[json.exception.out_of_range.403] (bytes 0-79) key 'missing' not found";
// the root has no path, and with JSON_DIAGNOSTICS then no bytes either (as basic_json)
std::string expected_missing = "[json.exception.out_of_range.403] ";
#if !JSON_DIAGNOSTICS
expected_missing += "(bytes 0-79) ";
#endif
expected_missing += "key 'missing' not found";
CHECK(exception_of_call([&] { static_cast<void>(d.root().at("missing")); }) == expected_missing);
}