Compare commits

..
Author SHA1 Message Date
Niels Lohmann 1d4baf429e docs: make the diff() and patch() documentation accurate; add tests
Document the exceptions to the diff round-trip invariant (NaN, discarded
values, number types), the path parameter, non-minimal patches, array and
object operation order, the ordered_json fallback, binary values in
serialized patches, the real complexity, duplicate member names in
operations, test on missing paths, and the exact out_of_range.403 message.
Add tests for these behaviors.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-26 16:15:27 +01:00
Niels LohmannandClaude Sonnet 5.5 cdbd969fe4 Throw out_of_range.411 for JSON Patch add into a null parent (#5917)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MKJf48vnGtTE25KscmjfaW
Signed-off-by: Niels Lohmann <niels.lohmann@gmail.com>
2026-10-25 13:22:10 +00:00
15 changed files with 365 additions and 1203 deletions

No files matched your search

+1 -2
View File
@@ -90,8 +90,7 @@ Strong exception safety: if an exception occurs, the original value stays intact
1. Constant.
2. Logarithmic in the size of the container.
3. Logarithmic in the size of the container.
4. Linear in the number of reference tokens of `ptr`; for each of them, logarithmic in the size of the object, or
constant for an array.
4. Logarithmic in the size of the container.
## Notes
+41 -4
View File
@@ -2,17 +2,27 @@
```cpp
static basic_json diff(const basic_json& source,
const basic_json& target);
const basic_json& target,
const string_t& path = "");
```
Creates a [JSON Patch](http://jsonpatch.com) so that value `source` can be changed into the value `target` by calling
[`patch`](patch.md) function.
For two JSON values `source` and `target`, the following code always yields `#!cpp true`:
For two JSON values `source` and `target`, the following code yields `#!cpp true`:
```cpp
source.patch(diff(source, target)) == target;
```
The equality holds up to [`operator==`](operator_eq.md), so it has the following exceptions:
- A value that is not equal to itself cannot round-trip. This applies to `NaN` (also nested in an array or object) and
to [discarded](is_discarded.md) values: `diff` emits a `replace` operation for them. A `NaN` in the patch is
serialized as `#!json null`, so a patch that was dumped and parsed again would set `#!json null`.
- Numbers that compare equal produce no operation, even if their types or signs differ: `diff(1, 1.0)`,
`diff(1u, 1)` (unsigned and signed integer) and `diff(0.0, -0.0)` all return `#!json []`. The patched value is then
the `source` and keeps its number type.
## Parameters
`source` (in)
@@ -21,6 +31,12 @@ source.patch(diff(source, target)) == target;
`target` (in)
: JSON value to compare against
`path` (in)
: a prefix that is put verbatim in front of the `path` member of every operation (default: `""`, which addresses the
root of the value). The prefix is neither escaped nor validated: `#!cpp diff(1, 2, "abc")` yields an operation with
the `path` `abc`, which is not a valid JSON Pointer. Use it with a valid JSON Pointer such as `/root` and escape
`~` and `/` in its reference tokens (see [JSON Pointer](../../features/json_pointer.md)).
## Return value
a JSON patch to convert the `source` to `target`
@@ -31,11 +47,32 @@ Strong guarantee: `source` and `target` are never modified.
## Complexity
Linear in the lengths of `source` and `target`.
Linear in the total number of values (including nested ones) in `source` and `target`, times the cost of a key-based
object operation (looking up a key in an object): logarithmic for [`json`](../json.md), and linear for
[`ordered_json`](../ordered_json.md) (see [`ordered_map` complexity](../ordered_map.md#complexity)). For
`ordered_json`, diffing two objects with `n` and `m` members therefore takes O((n+m)²) when their keys do not line up
(e.g., a key was added or the keys are in a different order). The size of the patch is proportional to the number of
operations times the length of their paths, so a small change deep inside a value yields a long `path`.
## Notes
Currently, only `remove`, `add`, and `replace` operations are generated.
- Currently, only `remove`, `add`, and `replace` operations are generated.
- The patch is correct, but not guaranteed to be minimal. For instance, `diff([1,2,3], [0,1,2,3])` yields three
`replace` operations and one `add` operation instead of a single insertion.
- Differences between two arrays are reported index by index. Elements that were added at the end of the array are added
with `add` operations on the path `/-` (appended in order), and elements that were removed are removed with `remove`
operations from the highest index down to the lowest, so that the remaining indices stay valid.
- For objects, the operations for the members of `source` come first (`replace`, `remove`, or the operations for nested
values, in the iteration order of `source`), followed by `add` operations for the members that only `target` has.
- For [`ordered_json`](../ordered_json.md), objects with the same keys in a different order are different. As a patch
cannot reorder members, `diff` removes all keys of the `source` object and adds them again in the order of the
`target`.
- If `source` and `target` differ as a whole (e.g., different types), the patch consists of a single `replace`
operation for the root of the value, i.e., with the `path` `""` (or the given `path` prefix).
- The `value` members of the patch hold copies of values of `target`. A [binary](../../features/binary_values.md) value
has no JSON representation: when a patch that contains one is serialized, it becomes an object such as
`#!json {"bytes":[1,3],"subtype":null}`, so a parsed patch no longer holds a binary value (and would set that object
instead).
## Examples
+6 -27
View File
@@ -92,20 +92,12 @@ Strong exception safety: if an exception occurs, the original value stays intact
For the **const** version, an object key or array index in `ptr` that does not exist is not reported by an
exception, but is undefined behavior (see the notes below). Use [`at`](at.md) for checked access.
If the **non-const** version throws, the value is left unchanged: values it created for earlier reference tokens
of `ptr` are removed again. This also holds for exceptions such as `#!cpp std::bad_alloc` or
`#!cpp std::length_error` thrown while padding an array up to a very large index. With custom
[template parameters](../../features/types/template_parameters.md), this requires that `erase()` of the object and
array types and the move operations of the custom base class do not throw.
## Complexity
1. Constant if `idx` is in the range of the array. Otherwise, linear in `idx - size()`.
2. Logarithmic in the size of the container.
3. Logarithmic in the size of the container.
4. Linear in the number of reference tokens of `ptr`; for each of them, logarithmic in the size of the object, or
constant for an array. When the non-const version pads an array with `#!json null` values, it is additionally
linear in `idx - size()` for that array.
4. Logarithmic in the size of the container.
## Notes
@@ -145,21 +137,10 @@ Strong exception safety: if an exception occurs, the original value stays intact
When the JSON pointer traverses intermediate levels that don't exist at all yet (not just a missing
leaf), each missing level is created as an array or an object depending on whether the corresponding
pointer token is a valid array index: the token `0`, a sequence of digits that does not begin with `0`,
or the token `-` creates an array, and every other token creates an object. For example, on an
initially `#!json null` value, `/foo/0/0/0` creates nested arrays, while `/foo/one/one/one` creates
nested objects. Tokens such as `01` or the empty token cannot be array indices (cf. RFC 6901, Sect. 4)
and therefore create objects, just as they would if the level already existed as an object. This is not
specified by the JSON Pointer RFC; it is this library's own, intentional disambiguation rule. See also
[JSON Pointer](../../features/json_pointer.md).
!!! warning "Large array indices"
As the array is filled up with `#!json null` values to the given index, the memory used grows with the index
and not with the size of the pointer: on a 64-bit platform, the index `10000000` allocates about 160 MB.
The library does not limit the index, so check the array indices of a JSON pointer from untrusted input
before using it with the non-const version. See
[Parsing untrusted input](../../features/parsing/untrusted_input.md#json-pointers-and-flattened-keys).
pointer token parses as a non-negative integer: a numeric token creates an array, a non-numeric token
creates an object. For example, on an initially `#!json null` value, `/foo/0/0/0` creates nested arrays,
while `/foo/one/one/one` creates nested objects. This is not specified by the JSON Pointer RFC; it is
this library's own, intentional disambiguation rule. See also [JSON Pointer](../../features/json_pointer.md).
!!! warning "Deprecation"
@@ -310,6 +291,4 @@ Strong exception safety: if an exception occurs, the original value stays intact
4. Added in version 2.0.0. A missing array index in the const version is guarded by a runtime assertion since
version 3.13.0. Throws [`parse_error.109`](../../home/exceptions.md#jsonexceptionparse_error109) instead of
[`out_of_range.404`](../../home/exceptions.md#jsonexceptionout_of_range404) for a one-character array index that is not
a digit (e.g., `/x`) in version 3.13.0, as it already did for longer ones. Fixed in version 3.13.0 to leave the value
unchanged if the non-const version throws (strong exception safety, as documented); before, values created for earlier
reference tokens of `ptr` were kept.
a digit (e.g., `/x`) in version 3.13.0, as it already did for longer ones.
+10 -2
View File
@@ -40,7 +40,7 @@ Strong guarantee: if an exception is thrown, there are no changes in the JSON va
where an existing element is required (the "path" of "replace", the "from" of "move" and "copy"); example:
`"array index '-' (3) is out of range"`.
- Throws [`out_of_range.403`](../../home/exceptions.md#jsonexceptionout_of_range403) if a JSON pointer inside the patch
could not be resolved successfully in the current JSON value; example: `"key baz not found"`.
could not be resolved successfully in the current JSON value; example: `"key 'baz' not found"`.
- Throws [`out_of_range.404`](../../home/exceptions.md#jsonexceptionout_of_range404) if a reference token of a JSON
pointer inside the patch cannot be resolved, e.g., `-` in a "remove" operation or `1a` for an array; example:
`"unresolved reference token '-'"`.
@@ -53,7 +53,10 @@ Strong guarantee: if an exception is thrown, there are no changes in the JSON va
- Throws [`out_of_range.414`](../../home/exceptions.md#jsonexceptionout_of_range414) if a "move" operation's "from"
location is a proper prefix of its "path" location.
- Throws [`other_error.501`](../../home/exceptions.md#jsonexceptionother_error501) if "test" operation was
unsuccessful.
unsuccessful. A "test" operation whose "path" does not exist is unsuccessful, too: the failed lookup is reported as a
failed test, even if the operation has no "value" member (which would otherwise be
[`parse_error.105`](../../home/exceptions.md#jsonexceptionparse_error105)). Which of the two errors is reported for
this combination depends on the order in which the operands of a comparison are evaluated, so do not rely on it.
## Complexity
@@ -65,6 +68,10 @@ the JSON value, the complexity can usually be neglected.
The application of a patch is atomic: Either all operations succeed and the patched document is returned or an exception
is thrown. In any case, the original value is not changed: the patch is applied to a copy of the value.
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
`remove` operation.
## Examples
??? example "Example: apply a JSON patch"
@@ -108,6 +115,7 @@ 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.
This includes a `null` parent, which was previously silently converted to an object, 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.
- Added [`out_of_range.414`](../../home/exceptions.md#jsonexceptionout_of_range414) and rejected a "move" operation whose "from" location is a proper
@@ -36,7 +36,7 @@ No guarantees, value may be corrupted by an unsuccessful patch operation.
where an existing element is required (the "path" of "replace", the "from" of "move" and "copy"); example:
`"array index '-' (3) is out of range"`.
- Throws [`out_of_range.403`](../../home/exceptions.md#jsonexceptionout_of_range403) if a JSON pointer inside the patch
could not be resolved successfully in the current JSON value; example: `"key baz not found"`.
could not be resolved successfully in the current JSON value; example: `"key 'baz' not found"`.
- Throws [`out_of_range.404`](../../home/exceptions.md#jsonexceptionout_of_range404) if a reference token of a JSON
pointer inside the patch cannot be resolved, e.g., `-` in a "remove" operation or `1a` for an array; example:
`"unresolved reference token '-'"`.
@@ -49,7 +49,10 @@ No guarantees, value may be corrupted by an unsuccessful patch operation.
- Throws [`out_of_range.414`](../../home/exceptions.md#jsonexceptionout_of_range414) if a "move" operation's "from"
location is a proper prefix of its "path" location.
- Throws [`other_error.501`](../../home/exceptions.md#jsonexceptionother_error501) if "test" operation was
unsuccessful.
unsuccessful. A "test" operation whose "path" does not exist is unsuccessful, too: the failed lookup is reported as a
failed test, even if the operation has no "value" member (which would otherwise be
[`parse_error.105`](../../home/exceptions.md#jsonexceptionparse_error105)). Which of the two errors is reported for
this combination depends on the order in which the operands of a comparison are evaluated, so do not rely on it.
## Complexity
@@ -62,6 +65,10 @@ Unlike [`patch`](patch.md), `patch_inplace` applies the operation "in place" and
That makes it faster for large documents by avoiding the copy. However, the JSON value might be corrupted if the
function throws an exception.
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
`remove` operation.
## Examples
??? example "Example: apply a JSON patch in place"
@@ -106,6 +113,7 @@ 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.
This includes a `null` parent, which was previously silently converted to an object, 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.
- Added [`out_of_range.414`](../../home/exceptions.md#jsonexceptionout_of_range414) and rejected a "move" operation whose "from" location is a proper
+18 -1
View File
@@ -13,6 +13,12 @@ to the current JSON value by executing all operations from the patch, yielding t
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.
!!! note "Duplicate member names"
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`](../api/basic_json/object_t.md#behavior)), so
`#!json {"op": "add", "op": "remove", "path": "/a"}` is a `remove` operation.
??? example "Example: apply a JSON Patch"
The following code shows how a JSON patch is applied to a value.
@@ -41,12 +47,23 @@ flowchart LR
!!! success "Invariant"
For two JSON values *source* and *target*, the following code yields always true:
For two JSON values *source* and *target*, the following code yields true:
```cpp
source.patch(diff(source, target)) == target;
```
The equality holds up to [`operator==`](../api/basic_json/operator_eq.md), with these exceptions:
- `NaN` and [discarded](../api/basic_json/is_discarded.md) values are not equal to themselves, so the invariant
cannot hold for them. `diff` emits a `replace` operation for such a value, and a `NaN` in a patch is serialized
as `#!json null`.
- Numbers that compare equal produce no operation even if their types differ: `diff(1, 1.0)`, `diff(1u, 1)`, and
`diff(0.0, -0.0)` return `#!json []`, so the patched value keeps the number type of *source*.
- A [binary value](binary_values.md) in a patch is serialized as an object
(`#!json {"bytes":[...],"subtype":null}`), so a patch that was dumped and parsed again no longer holds a binary
value.
??? example "Example: create a JSON Patch from the difference of two values"
The following code shows how a JSON patch is created as a diff for two JSON values.
@@ -130,17 +130,6 @@ parameters of [`parse()`](../../api/basic_json/parse.md) or [`accept()`](../../a
enable either for input whose conformance you cannot otherwise control, since interoperability with strictly
conforming JSON consumers is exactly what the default rejects.
## JSON Pointers and flattened keys
A [JSON Pointer](../json_pointer.md) can come from untrusted input as well, for instance as a path in a request or as a
key of a flattened object. The non-const [`operator[]`](../../api/basic_json/operator%5B%5D.md) with a JSON Pointer and
[`unflatten`](../../api/basic_json/unflatten.md) create missing values, and when an array index lies beyond the end of
an array, they fill the array with `#!json null` values up to that index. The memory used therefore grows with the
index, not with the size of the pointer: on a 64-bit platform, the index `10000000` allocates about 160 MB, the index
`100000000` about 1.6 GB. The library does not limit the index. For pointers from untrusted input, check the array
indices (or the pointer as a whole) before using them, or use [`at`](../../api/basic_json/at.md) or
[`contains`](../../api/basic_json/contains.md), which never add values.
## Checklist
- Wrap parsing in a `#!cpp try`/`#!cpp catch` block, or use `allow_exceptions=false`/`accept()` if your environment
@@ -156,8 +145,6 @@ indices (or the pointer as a whole) before using them, or use [`at`](../../api/b
`JSON_STRICT_NUL_HANDLING`.
- Decide whether duplicate object keys should be an error for your application, and add a callback if so.
- Do not enable `ignore_comments` or `ignore_trailing_commas` for input that must be strictly conforming JSON.
- Check the array indices of untrusted JSON Pointers and flattened keys before passing them to the non-const
`operator[]` or `unflatten()`, which fill arrays with `#!json null` values up to the index.
For the broader design rationale and how it is tested (fuzzing, sanitizers, static analysis), see the
[assurance case](../../community/assurance_case.md) and [quality assurance](../../community/quality_assurance.md). To
+2
View File
@@ -1100,6 +1100,8 @@ A JSON Patch `add` operation cannot be applied because the target location's par
This exception was added in version 3.13.0. Before that, this situation hit an internal assertion (aborting the program in debug builds) or was silently ignored when assertions were disabled.
A `null` parent is rejected as well. Before version 3.13.0, an `add` into a `null` parent silently converted it to an object (e.g., `add` at `/foo/bar` turned `{"foo":null}` into `{"foo":{"bar":1}}`).
### json.exception.out_of_range.412
BSON stores the length of documents, arrays, strings, and binary values in a signed 32-bit integer, and MessagePack
+26 -488
View File
@@ -8,7 +8,7 @@
#pragma once
#include <algorithm> // copy
#include <algorithm> // all_of, copy
#include <array> // array
#include <cctype> // isdigit
#include <cerrno> // errno, ERANGE
@@ -16,11 +16,9 @@
#include <cstdint> // uint64_t
#include <cstdlib> // strtoull
#include <cstring> // memchr, memcmp, memcpy
#include <functional> // less
#ifndef JSON_NO_IO
#include <iosfwd> // ostream
#endif // JSON_NO_IO
#include <iterator> // next
#include <limits> // max
#include <string> // string
#include <utility> // move
@@ -30,7 +28,6 @@
#include <nlohmann/detail/macro_scope.hpp>
#include <nlohmann/detail/meta/cpp_future.hpp>
#include <nlohmann/detail/meta/type_traits.hpp>
#include <nlohmann/detail/meta/void_t.hpp>
#include <nlohmann/detail/string_concat.hpp>
#include <nlohmann/detail/string_escape.hpp>
#include <nlohmann/detail/string_utils.hpp>
@@ -137,26 +134,6 @@ JSON_HEDLEY_ALWAYS_INLINE array_index_status parse_array_index(const StringType&
return array_index_status::ok;
}
/// whether two keys of type @a StringType are equivalent under @a Compare
/// exactly if they are equal: std::less<StringType> and std::less<>
template<typename Compare, typename StringType>
struct is_less_comparator : std::false_type {};
template<typename StringType>
struct is_less_comparator<std::less<StringType>, StringType> : std::true_type {};
template<typename StringType>
struct is_less_comparator<std::less<void>, StringType> : std::true_type {};
/// whether the objects of type @a ObjectType find a key of type @a StringType
/// exactly if it equals a stored key
template<typename ObjectType, typename StringType, typename = void>
struct finds_equal_keys : std::false_type {};
template<typename ObjectType, typename StringType>
struct finds_equal_keys<ObjectType, StringType, void_t<typename ObjectType::key_compare, typename ObjectType::key_type>>
: conjunction<is_less_comparator<typename ObjectType::key_compare, StringType>, std::is_same<typename ObjectType::key_type, StringType>> {};
} // namespace detail
/// @brief JSON Pointer defines a string syntax for identifying a specific value within a JSON document
@@ -612,228 +589,6 @@ class json_pointer
return object_member_impl(object, key, detail::priority_tag<1> {});
}
/// @brief append a null value to @a array, like operator[] with the size of @a array
template<typename BasicJsonType>
static BasicJsonType& array_append(BasicJsonType& array)
{
auto& elements = *array.m_data.m_value.array;
const auto old_capacity = array.container_capacity();
elements.emplace_back();
return array.set_parent_after_insert(elements.back(), old_capacity);
}
/// @brief add @a key, which is missing, to @a object
template<typename BasicJsonType>
static auto object_add_impl(BasicJsonType& object, const string_t& key, detail::priority_tag<1> /*unused*/)
-> decltype(object.m_data.m_value.object->emplace(key, nullptr), std::declval<BasicJsonType&>())
{
const auto old_capacity = object.container_capacity();
const auto it = object.m_data.m_value.object->emplace(key, nullptr).first;
return object.set_parent_after_insert(it->second, old_capacity);
}
template<typename BasicJsonType>
static BasicJsonType& object_add_impl(BasicJsonType& object, const string_t& key, detail::priority_tag<0> /*unused*/)
{
return object.operator[](key);
}
template<typename BasicJsonType>
static BasicJsonType& object_add(BasicJsonType& object, const string_t& key)
{
return object_add_impl(object, key, detail::priority_tag<1> {});
}
/// @brief search @a key in @a members: the result is where the key is or,
/// for objects that can insert at a position (std::map), where it belongs
template<typename ObjectType>
static auto object_search_impl(ObjectType& members, const string_t& key, detail::priority_tag<1> /*unused*/)
-> decltype(members.key_comp()(key, members.lower_bound(key)->first), members.lower_bound(key))
{
return members.lower_bound(key);
}
template<typename ObjectType>
static auto object_search_impl(ObjectType& members, const string_t& key, detail::priority_tag<0> /*unused*/)
-> decltype(members.find(key))
{
return members.find(key);
}
template<typename ObjectType>
static auto object_search(ObjectType& members, const string_t& key)
-> decltype(object_search_impl(members, key, detail::priority_tag<1> {}))
{
return object_search_impl(members, key, detail::priority_tag<1> {});
}
/// @brief whether @a it, the result of @ref object_search for @a key, points to @a key
///
/// With std::less, the key at the lower bound is equivalent to @a key
/// exactly if it is equal to it, and comparing for equality is cheaper.
template<typename ObjectType, typename Iterator,
detail::enable_if_t<detail::finds_equal_keys<ObjectType, string_t>::value, int> = 0>
static auto object_search_found_impl(ObjectType& members, const Iterator& it, const string_t& key, detail::priority_tag<2> /*unused*/)
-> decltype(members.lower_bound(key), it->first == key)
{
return it != members.end() && it->first == key;
}
template<typename ObjectType, typename Iterator>
static auto object_search_found_impl(ObjectType& members, const Iterator& it, const string_t& key, detail::priority_tag<1> /*unused*/)
-> decltype(members.key_comp()(key, it->first), members.lower_bound(key), bool())
{
return it != members.end() && !members.key_comp()(key, it->first);
}
template<typename ObjectType, typename Iterator>
static bool object_search_found_impl(ObjectType& members, const Iterator& it, const string_t& /*key*/, detail::priority_tag<0> /*unused*/)
{
return it != members.end();
}
template<typename ObjectType, typename Iterator>
static bool object_search_found(ObjectType& members, const Iterator& it, const string_t& key)
{
return object_search_found_impl(members, it, key, detail::priority_tag<2> {});
}
/// @brief insert @a key, which is missing, into @a object at @a it, the
/// result of @ref object_search for @a key
template<typename BasicJsonType, typename Iterator>
static auto object_insert_impl(BasicJsonType& object, const Iterator& it, const string_t& key, detail::priority_tag<1> /*unused*/)
-> decltype(object.m_data.m_value.object->emplace_hint(it, key, nullptr), std::declval<BasicJsonType&>())
{
const auto old_capacity = object.container_capacity();
const auto pos = object.m_data.m_value.object->emplace_hint(it, key, nullptr);
return object.set_parent_after_insert(pos->second, old_capacity);
}
template<typename BasicJsonType, typename Iterator>
static BasicJsonType& object_insert_impl(BasicJsonType& object, const Iterator& /*it*/, const string_t& key, detail::priority_tag<0> /*unused*/)
{
return object_add(object, key);
}
template<typename BasicJsonType, typename Iterator>
static BasicJsonType& object_insert(BasicJsonType& object, const Iterator& it, const string_t& key)
{
return object_insert_impl(object, it, key, detail::priority_tag<1> {});
}
/*!
@brief undoes the change @ref get_unchecked_create made to an existing value
@ref get_unchecked walks existing values without changing them. At the
first reference token that does not select an existing value, @ref
get_unchecked_create makes exactly one change to an existing value: it
converts a null value, inserts a key into an object, or grows an array.
Everything it creates afterwards lies below that change. If a later step
throws, the destructor of this guard reverts that one change, which
removes everything created after it, so the value is left as it was
(strong exception safety). The guard needs no try/catch and therefore also
compiles with JSON_NOEXCEPTION.
Reverting the change must not throw, as it happens in a destructor. It
erases the inserted member by its iterator (no key is converted or
compared, nothing is allocated), erases the added elements at the end of
the array, or swaps the null value back. This holds for std::map,
std::vector, ordered_map, and every object_t, array_t, and
json_base_class_t whose erase() and move operations do not throw.
Between recording and reverting, nothing is inserted into or erased from
the object or array, so the iterator stays valid.
*/
template<typename BasicJsonType>
class get_unchecked_rollback
{
public:
using object_iterator = typename BasicJsonType::object_t::iterator;
/// record that @a member was inserted into the object @a value
get_unchecked_rollback(BasicJsonType* value, const object_iterator& member)
: m_value(value)
, m_member(member)
, m_change(change::key_inserted)
{}
/// record the change that the next reference token makes to the null value or array @a value
explicit get_unchecked_rollback(BasicJsonType* value)
: m_value(value)
{
if (value->is_null())
{
// keep the null value (with its base class and positions) and
// continue with a fresh null value in its place, which is what
// assigning an array or object to it would have left there
m_saved_null.swap(*value);
m_change = change::null_converted;
}
else
{
JSON_ASSERT(value->is_array());
m_old_size = value->m_data.m_value.array->size();
m_change = change::array_grown;
}
}
get_unchecked_rollback(const get_unchecked_rollback&) = delete;
get_unchecked_rollback(get_unchecked_rollback&&) = delete;
get_unchecked_rollback& operator=(const get_unchecked_rollback&) = delete;
get_unchecked_rollback& operator=(get_unchecked_rollback&&) = delete;
~get_unchecked_rollback()
{
if (JSON_HEDLEY_UNLIKELY(m_change != change::none))
{
undo();
}
}
/// keep the change
void commit() noexcept
{
m_change = change::none;
}
private:
JSON_HEDLEY_NEVER_INLINE void undo() noexcept
{
switch (m_change)
{
case change::null_converted:
// the values created since go away with m_saved_null
m_value->swap(m_saved_null);
break;
case change::key_inserted:
m_value->m_data.m_value.object->erase(m_member);
break;
case change::array_grown:
{
auto& elements = *m_value->m_data.m_value.array;
elements.erase(std::next(elements.begin(), static_cast<typename BasicJsonType::difference_type>(m_old_size)), elements.end());
break;
}
case change::none:
default:
break;
}
}
enum class change
{
none,
null_converted,
key_inserted,
array_grown
};
BasicJsonType* m_value;
object_iterator m_member {};
typename BasicJsonType::size_type m_old_size = 0;
change m_change = change::none;
BasicJsonType m_saved_null {};
};
JSON_PRIVATE_UNLESS_TESTED:
json_pointer top() const
{
@@ -1217,245 +972,43 @@ class json_pointer
Complexity: Linear in the length of the JSON pointer.
Exception safety: Strong guarantee. This function walks existing values
until the first reference token that does not select one. If that is the
last reference token, inserting the key into the object or growing the
array is strongly exception safe by itself; any other change is made by
@ref get_unchecked_create, which undoes it if a later step throws.
@throw parse_error.106 if an array index of an existing array begins with '0'
@throw parse_error.109 if an array index of an existing array was not a number
@throw out_of_range.404 if the JSON pointer can not be resolved, or if an
array index is too large for strtoull
@throw out_of_range.410 if an array index exceeds size_type
@throw std::length_error if padding an array exceeds its maximal size
@throw std::bad_alloc if an allocation fails
@throw parse_error.106 if an array index begins with '0'
@throw parse_error.109 if an array index was not a number
@throw out_of_range.404 if the JSON pointer can not be resolved
*/
template<typename BasicJsonType>
BasicJsonType& get_unchecked(BasicJsonType* ptr) const
{
const auto* token = reference_tokens.data();
const auto* const end = token + reference_tokens.size();
if (token == end)
for (const auto& reference_token : reference_tokens)
{
return *ptr;
}
if (ptr->is_null())
{
// the null value is converted to an array or an object
return get_unchecked_create(ptr, token, end);
}
// walk existing values; the walk itself inserts nothing, which keeps
// the loop fast (insertions are done below or in get_unchecked_create)
typename BasicJsonType::object_t::iterator it;
do
{
if (ptr->is_object())
{
auto& members = *ptr->m_data.m_value.object;
it = object_search(members, *token);
if (!object_search_found(members, it, *token))
{
// the key is inserted below or in get_unchecked_create
break;
}
ptr = &it->second;
}
else if (ptr->is_array())
{
if (is_dash(*token))
{
if (token + 1 == end)
{
// explicitly treat "-" as index beyond the end
return array_append(*ptr);
}
// an element is appended in get_unchecked_create
break;
}
if (token + 1 == end)
{
// convert array index to number; unchecked access
return ptr->operator[](array_index<BasicJsonType>(*token));
}
const auto idx = array_index<BasicJsonType>(*token);
if (idx >= ptr->m_data.m_value.array->size())
{
// the array is padded in get_unchecked_create
break;
}
ptr = &(*ptr->m_data.m_value.array)[idx];
}
else if (ptr->is_null())
{
// the null value is converted in get_unchecked_create
break;
}
else
{
// primitive value with a reference token left
throw_unresolved(*token, ptr);
}
}
while (++token != end);
if (token == end)
{
return *ptr;
}
if (token + 1 == end && ptr->is_object())
{
// insert the missing key where the walk found it belongs
return object_insert(*ptr, it, *token);
}
return get_unchecked_create(ptr, token, end);
}
/*!
@brief create the values for the reference tokens from @a first on
Continues @ref get_unchecked at the first reference token that does not
select an existing value. That token changes @a ptr, an existing value (see
@ref get_unchecked_rollback); all later tokens only change values created
here.
If the emplace() of the object type finds a member that the lookup in @ref
get_unchecked did not find (the two disagree, e.g. for fifo_map), nothing
is inserted, and the walk continues with that member as in @ref
get_unchecked, which is what basic_json::operator[] does.
@param[in] ptr the value the reference token @a first is applied to
@param[in] first the first reference token to resolve (an element of
@ref reference_tokens)
@param[in] end the end of @ref reference_tokens
@throw parse_error.106 if an array index of an existing array begins with
'0' (only after emplace() found a member, see above)
@throw parse_error.109 if an array index of an existing array was not a
number (only after emplace() found a member)
@throw out_of_range.404 if an array index is too large for strtoull, or (only
after emplace() found a member) if the JSON pointer
can not be resolved
@throw out_of_range.410 if an array index exceeds size_type
@throw std::length_error if padding an array exceeds its maximal size
@throw std::bad_alloc if an allocation fails
*/
template<typename BasicJsonType>
JSON_HEDLEY_NEVER_INLINE static BasicJsonType& get_unchecked_create(BasicJsonType* ptr, const string_t* first, const string_t* end)
{
const auto* token = first;
while (ptr->is_object())
{
const auto old_capacity = ptr->container_capacity();
const auto inserted = ptr->m_data.m_value.object->emplace(*token, nullptr);
if (JSON_HEDLEY_LIKELY(inserted.second))
{
get_unchecked_rollback<BasicJsonType> rollback(ptr, inserted.first);
BasicJsonType& result = create_values(&ptr->set_parent_after_insert(inserted.first->second, old_capacity), token + 1, end);
rollback.commit();
return result;
}
// nothing was inserted: walk existing values from the member
// emplace() found on, like get_unchecked
ptr = &inserted.first->second;
while (++token != end)
{
if (ptr->is_object())
{
auto& members = *ptr->m_data.m_value.object;
const auto it = object_search(members, *token);
if (!object_search_found(members, it, *token))
{
break;
}
ptr = &it->second;
}
else if (ptr->is_array())
{
if (is_dash(*token))
{
break;
}
const auto idx = array_index<BasicJsonType>(*token);
if (idx >= ptr->m_data.m_value.array->size())
{
break;
}
ptr = &(*ptr->m_data.m_value.array)[idx];
}
else if (ptr->is_null())
{
break;
}
else
{
// primitive value with a reference token left
throw_unresolved(*token, ptr);
}
}
if (token == end)
{
return *ptr;
}
}
get_unchecked_rollback<BasicJsonType> rollback(ptr);
BasicJsonType& result = create_values(ptr, token, end);
rollback.commit();
return result;
}
/*!
@brief the loop of @ref get_unchecked_create
@a ptr is a null value or an array. Every value created here is a fresh
one, so every object a key is added to is empty.
@throw out_of_range.404 if an array index is too large for strtoull
@throw out_of_range.410 if an array index exceeds size_type
@throw std::length_error if padding an array exceeds its maximal size
@throw std::bad_alloc if an allocation fails
*/
template<typename BasicJsonType>
static BasicJsonType& create_values(BasicJsonType* ptr, const string_t* first, const string_t* end)
{
for (const auto* token = first; token != end; ++token)
{
const auto& reference_token = *token;
// convert null values to arrays or objects before continuing
if (ptr->is_null())
{
// change value to an array for array indices or "-" or to an
// object otherwise; every null value here is a fresh one (see
// get_unchecked_rollback), so converting it in place leaves
// the same value as assigning an array or object to it
if (is_array_index_token(reference_token) || is_dash(reference_token))
// check if the reference token is a number
const bool nums =
std::all_of(reference_token.begin(), reference_token.end(),
[](const unsigned char x)
{
ptr->template convert_null_to<typename BasicJsonType::array_t>();
}
else
{
ptr->template convert_null_to<typename BasicJsonType::object_t>();
}
return std::isdigit(x);
});
// change value to an array for numbers or "-" or to object otherwise
*ptr = (nums || reference_token == "-")
? detail::value_t::array
: detail::value_t::object;
}
if (ptr->is_object())
{
// the object is new and empty
ptr = &object_add(*ptr, reference_token);
// use unchecked object access
ptr = &object_member(*ptr, reference_token);
}
else
else if (ptr->is_array())
{
// only the first value is not created here, and it is a null
// value or an array (see get_unchecked_create)
JSON_ASSERT(ptr->is_array());
if (is_dash(reference_token))
if (reference_token == "-")
{
// explicitly treat "-" as index beyond the end
ptr = &array_append(*ptr);
ptr = &ptr->operator[](ptr->m_data.m_value.array->size());
}
else
{
@@ -1463,31 +1016,16 @@ class json_pointer
ptr = &ptr->operator[](array_index<BasicJsonType>(reference_token));
}
}
else
{
// primitive value with a reference token left
throw_unresolved(reference_token, ptr);
}
}
return *ptr;
}
/// whether @a s is a valid array index, that is a nonempty sequence of
/// digits without a leading '0' (cf. RFC 6901, Sect. 4); tokens that could
/// never be a valid array index (such as "01" or "") are object keys
static bool is_array_index_token(const string_t& s)
{
const std::size_t size = s.size();
if (size == 0 || (size > 1 && s[0] == '0'))
{
return false;
}
for (std::size_t i = 0; i < size; ++i)
{
if (std::isdigit(static_cast<unsigned char>(s[i])) == 0)
{
return false;
}
}
return true;
}
/*!
@throw parse_error.106 if an array index begins with '0'
@throw parse_error.109 if an array index was not a number
+1 -1
View File
@@ -7003,7 +7003,6 @@ public:
switch (parent.m_data.m_type)
{
case value_t::null:
case value_t::object:
{
// use operator[] to add value
@@ -7035,6 +7034,7 @@ public:
// the parent of an "add" target must be an object or array
// (see #4292)
case value_t::null:
case value_t::string:
case value_t::boolean:
case value_t::number_integer:
+27 -490
View File
@@ -23585,7 +23585,7 @@ NLOHMANN_JSON_NAMESPACE_END
#include <algorithm> // copy
#include <algorithm> // all_of, copy
#include <array> // array
#include <cctype> // isdigit
#include <cerrno> // errno, ERANGE
@@ -23593,11 +23593,9 @@ NLOHMANN_JSON_NAMESPACE_END
#include <cstdint> // uint64_t
#include <cstdlib> // strtoull
#include <cstring> // memchr, memcmp, memcpy
#include <functional> // less
#ifndef JSON_NO_IO
#include <iosfwd> // ostream
#endif // JSON_NO_IO
#include <iterator> // next
#include <limits> // max
#include <string> // string
#include <utility> // move
@@ -23611,8 +23609,6 @@ NLOHMANN_JSON_NAMESPACE_END
// #include <nlohmann/detail/meta/type_traits.hpp>
// #include <nlohmann/detail/meta/void_t.hpp>
// #include <nlohmann/detail/string_concat.hpp>
// #include <nlohmann/detail/string_escape.hpp>
@@ -23723,26 +23719,6 @@ JSON_HEDLEY_ALWAYS_INLINE array_index_status parse_array_index(const StringType&
return array_index_status::ok;
}
/// whether two keys of type @a StringType are equivalent under @a Compare
/// exactly if they are equal: std::less<StringType> and std::less<>
template<typename Compare, typename StringType>
struct is_less_comparator : std::false_type {};
template<typename StringType>
struct is_less_comparator<std::less<StringType>, StringType> : std::true_type {};
template<typename StringType>
struct is_less_comparator<std::less<void>, StringType> : std::true_type {};
/// whether the objects of type @a ObjectType find a key of type @a StringType
/// exactly if it equals a stored key
template<typename ObjectType, typename StringType, typename = void>
struct finds_equal_keys : std::false_type {};
template<typename ObjectType, typename StringType>
struct finds_equal_keys<ObjectType, StringType, void_t<typename ObjectType::key_compare, typename ObjectType::key_type>>
: conjunction<is_less_comparator<typename ObjectType::key_compare, StringType>, std::is_same<typename ObjectType::key_type, StringType>> {};
} // namespace detail
/// @brief JSON Pointer defines a string syntax for identifying a specific value within a JSON document
@@ -24198,228 +24174,6 @@ class json_pointer
return object_member_impl(object, key, detail::priority_tag<1> {});
}
/// @brief append a null value to @a array, like operator[] with the size of @a array
template<typename BasicJsonType>
static BasicJsonType& array_append(BasicJsonType& array)
{
auto& elements = *array.m_data.m_value.array;
const auto old_capacity = array.container_capacity();
elements.emplace_back();
return array.set_parent_after_insert(elements.back(), old_capacity);
}
/// @brief add @a key, which is missing, to @a object
template<typename BasicJsonType>
static auto object_add_impl(BasicJsonType& object, const string_t& key, detail::priority_tag<1> /*unused*/)
-> decltype(object.m_data.m_value.object->emplace(key, nullptr), std::declval<BasicJsonType&>())
{
const auto old_capacity = object.container_capacity();
const auto it = object.m_data.m_value.object->emplace(key, nullptr).first;
return object.set_parent_after_insert(it->second, old_capacity);
}
template<typename BasicJsonType>
static BasicJsonType& object_add_impl(BasicJsonType& object, const string_t& key, detail::priority_tag<0> /*unused*/)
{
return object.operator[](key);
}
template<typename BasicJsonType>
static BasicJsonType& object_add(BasicJsonType& object, const string_t& key)
{
return object_add_impl(object, key, detail::priority_tag<1> {});
}
/// @brief search @a key in @a members: the result is where the key is or,
/// for objects that can insert at a position (std::map), where it belongs
template<typename ObjectType>
static auto object_search_impl(ObjectType& members, const string_t& key, detail::priority_tag<1> /*unused*/)
-> decltype(members.key_comp()(key, members.lower_bound(key)->first), members.lower_bound(key))
{
return members.lower_bound(key);
}
template<typename ObjectType>
static auto object_search_impl(ObjectType& members, const string_t& key, detail::priority_tag<0> /*unused*/)
-> decltype(members.find(key))
{
return members.find(key);
}
template<typename ObjectType>
static auto object_search(ObjectType& members, const string_t& key)
-> decltype(object_search_impl(members, key, detail::priority_tag<1> {}))
{
return object_search_impl(members, key, detail::priority_tag<1> {});
}
/// @brief whether @a it, the result of @ref object_search for @a key, points to @a key
///
/// With std::less, the key at the lower bound is equivalent to @a key
/// exactly if it is equal to it, and comparing for equality is cheaper.
template<typename ObjectType, typename Iterator,
detail::enable_if_t<detail::finds_equal_keys<ObjectType, string_t>::value, int> = 0>
static auto object_search_found_impl(ObjectType& members, const Iterator& it, const string_t& key, detail::priority_tag<2> /*unused*/)
-> decltype(members.lower_bound(key), it->first == key)
{
return it != members.end() && it->first == key;
}
template<typename ObjectType, typename Iterator>
static auto object_search_found_impl(ObjectType& members, const Iterator& it, const string_t& key, detail::priority_tag<1> /*unused*/)
-> decltype(members.key_comp()(key, it->first), members.lower_bound(key), bool())
{
return it != members.end() && !members.key_comp()(key, it->first);
}
template<typename ObjectType, typename Iterator>
static bool object_search_found_impl(ObjectType& members, const Iterator& it, const string_t& /*key*/, detail::priority_tag<0> /*unused*/)
{
return it != members.end();
}
template<typename ObjectType, typename Iterator>
static bool object_search_found(ObjectType& members, const Iterator& it, const string_t& key)
{
return object_search_found_impl(members, it, key, detail::priority_tag<2> {});
}
/// @brief insert @a key, which is missing, into @a object at @a it, the
/// result of @ref object_search for @a key
template<typename BasicJsonType, typename Iterator>
static auto object_insert_impl(BasicJsonType& object, const Iterator& it, const string_t& key, detail::priority_tag<1> /*unused*/)
-> decltype(object.m_data.m_value.object->emplace_hint(it, key, nullptr), std::declval<BasicJsonType&>())
{
const auto old_capacity = object.container_capacity();
const auto pos = object.m_data.m_value.object->emplace_hint(it, key, nullptr);
return object.set_parent_after_insert(pos->second, old_capacity);
}
template<typename BasicJsonType, typename Iterator>
static BasicJsonType& object_insert_impl(BasicJsonType& object, const Iterator& /*it*/, const string_t& key, detail::priority_tag<0> /*unused*/)
{
return object_add(object, key);
}
template<typename BasicJsonType, typename Iterator>
static BasicJsonType& object_insert(BasicJsonType& object, const Iterator& it, const string_t& key)
{
return object_insert_impl(object, it, key, detail::priority_tag<1> {});
}
/*!
@brief undoes the change @ref get_unchecked_create made to an existing value
@ref get_unchecked walks existing values without changing them. At the
first reference token that does not select an existing value, @ref
get_unchecked_create makes exactly one change to an existing value: it
converts a null value, inserts a key into an object, or grows an array.
Everything it creates afterwards lies below that change. If a later step
throws, the destructor of this guard reverts that one change, which
removes everything created after it, so the value is left as it was
(strong exception safety). The guard needs no try/catch and therefore also
compiles with JSON_NOEXCEPTION.
Reverting the change must not throw, as it happens in a destructor. It
erases the inserted member by its iterator (no key is converted or
compared, nothing is allocated), erases the added elements at the end of
the array, or swaps the null value back. This holds for std::map,
std::vector, ordered_map, and every object_t, array_t, and
json_base_class_t whose erase() and move operations do not throw.
Between recording and reverting, nothing is inserted into or erased from
the object or array, so the iterator stays valid.
*/
template<typename BasicJsonType>
class get_unchecked_rollback
{
public:
using object_iterator = typename BasicJsonType::object_t::iterator;
/// record that @a member was inserted into the object @a value
get_unchecked_rollback(BasicJsonType* value, const object_iterator& member)
: m_value(value)
, m_member(member)
, m_change(change::key_inserted)
{}
/// record the change that the next reference token makes to the null value or array @a value
explicit get_unchecked_rollback(BasicJsonType* value)
: m_value(value)
{
if (value->is_null())
{
// keep the null value (with its base class and positions) and
// continue with a fresh null value in its place, which is what
// assigning an array or object to it would have left there
m_saved_null.swap(*value);
m_change = change::null_converted;
}
else
{
JSON_ASSERT(value->is_array());
m_old_size = value->m_data.m_value.array->size();
m_change = change::array_grown;
}
}
get_unchecked_rollback(const get_unchecked_rollback&) = delete;
get_unchecked_rollback(get_unchecked_rollback&&) = delete;
get_unchecked_rollback& operator=(const get_unchecked_rollback&) = delete;
get_unchecked_rollback& operator=(get_unchecked_rollback&&) = delete;
~get_unchecked_rollback()
{
if (JSON_HEDLEY_UNLIKELY(m_change != change::none))
{
undo();
}
}
/// keep the change
void commit() noexcept
{
m_change = change::none;
}
private:
JSON_HEDLEY_NEVER_INLINE void undo() noexcept
{
switch (m_change)
{
case change::null_converted:
// the values created since go away with m_saved_null
m_value->swap(m_saved_null);
break;
case change::key_inserted:
m_value->m_data.m_value.object->erase(m_member);
break;
case change::array_grown:
{
auto& elements = *m_value->m_data.m_value.array;
elements.erase(std::next(elements.begin(), static_cast<typename BasicJsonType::difference_type>(m_old_size)), elements.end());
break;
}
case change::none:
default:
break;
}
}
enum class change
{
none,
null_converted,
key_inserted,
array_grown
};
BasicJsonType* m_value;
object_iterator m_member {};
typename BasicJsonType::size_type m_old_size = 0;
change m_change = change::none;
BasicJsonType m_saved_null {};
};
JSON_PRIVATE_UNLESS_TESTED:
json_pointer top() const
{
@@ -24803,245 +24557,43 @@ class json_pointer
Complexity: Linear in the length of the JSON pointer.
Exception safety: Strong guarantee. This function walks existing values
until the first reference token that does not select one. If that is the
last reference token, inserting the key into the object or growing the
array is strongly exception safe by itself; any other change is made by
@ref get_unchecked_create, which undoes it if a later step throws.
@throw parse_error.106 if an array index of an existing array begins with '0'
@throw parse_error.109 if an array index of an existing array was not a number
@throw out_of_range.404 if the JSON pointer can not be resolved, or if an
array index is too large for strtoull
@throw out_of_range.410 if an array index exceeds size_type
@throw std::length_error if padding an array exceeds its maximal size
@throw std::bad_alloc if an allocation fails
@throw parse_error.106 if an array index begins with '0'
@throw parse_error.109 if an array index was not a number
@throw out_of_range.404 if the JSON pointer can not be resolved
*/
template<typename BasicJsonType>
BasicJsonType& get_unchecked(BasicJsonType* ptr) const
{
const auto* token = reference_tokens.data();
const auto* const end = token + reference_tokens.size();
if (token == end)
for (const auto& reference_token : reference_tokens)
{
return *ptr;
}
if (ptr->is_null())
{
// the null value is converted to an array or an object
return get_unchecked_create(ptr, token, end);
}
// walk existing values; the walk itself inserts nothing, which keeps
// the loop fast (insertions are done below or in get_unchecked_create)
typename BasicJsonType::object_t::iterator it;
do
{
if (ptr->is_object())
{
auto& members = *ptr->m_data.m_value.object;
it = object_search(members, *token);
if (!object_search_found(members, it, *token))
{
// the key is inserted below or in get_unchecked_create
break;
}
ptr = &it->second;
}
else if (ptr->is_array())
{
if (is_dash(*token))
{
if (token + 1 == end)
{
// explicitly treat "-" as index beyond the end
return array_append(*ptr);
}
// an element is appended in get_unchecked_create
break;
}
if (token + 1 == end)
{
// convert array index to number; unchecked access
return ptr->operator[](array_index<BasicJsonType>(*token));
}
const auto idx = array_index<BasicJsonType>(*token);
if (idx >= ptr->m_data.m_value.array->size())
{
// the array is padded in get_unchecked_create
break;
}
ptr = &(*ptr->m_data.m_value.array)[idx];
}
else if (ptr->is_null())
{
// the null value is converted in get_unchecked_create
break;
}
else
{
// primitive value with a reference token left
throw_unresolved(*token, ptr);
}
}
while (++token != end);
if (token == end)
{
return *ptr;
}
if (token + 1 == end && ptr->is_object())
{
// insert the missing key where the walk found it belongs
return object_insert(*ptr, it, *token);
}
return get_unchecked_create(ptr, token, end);
}
/*!
@brief create the values for the reference tokens from @a first on
Continues @ref get_unchecked at the first reference token that does not
select an existing value. That token changes @a ptr, an existing value (see
@ref get_unchecked_rollback); all later tokens only change values created
here.
If the emplace() of the object type finds a member that the lookup in @ref
get_unchecked did not find (the two disagree, e.g. for fifo_map), nothing
is inserted, and the walk continues with that member as in @ref
get_unchecked, which is what basic_json::operator[] does.
@param[in] ptr the value the reference token @a first is applied to
@param[in] first the first reference token to resolve (an element of
@ref reference_tokens)
@param[in] end the end of @ref reference_tokens
@throw parse_error.106 if an array index of an existing array begins with
'0' (only after emplace() found a member, see above)
@throw parse_error.109 if an array index of an existing array was not a
number (only after emplace() found a member)
@throw out_of_range.404 if an array index is too large for strtoull, or (only
after emplace() found a member) if the JSON pointer
can not be resolved
@throw out_of_range.410 if an array index exceeds size_type
@throw std::length_error if padding an array exceeds its maximal size
@throw std::bad_alloc if an allocation fails
*/
template<typename BasicJsonType>
JSON_HEDLEY_NEVER_INLINE static BasicJsonType& get_unchecked_create(BasicJsonType* ptr, const string_t* first, const string_t* end)
{
const auto* token = first;
while (ptr->is_object())
{
const auto old_capacity = ptr->container_capacity();
const auto inserted = ptr->m_data.m_value.object->emplace(*token, nullptr);
if (JSON_HEDLEY_LIKELY(inserted.second))
{
get_unchecked_rollback<BasicJsonType> rollback(ptr, inserted.first);
BasicJsonType& result = create_values(&ptr->set_parent_after_insert(inserted.first->second, old_capacity), token + 1, end);
rollback.commit();
return result;
}
// nothing was inserted: walk existing values from the member
// emplace() found on, like get_unchecked
ptr = &inserted.first->second;
while (++token != end)
{
if (ptr->is_object())
{
auto& members = *ptr->m_data.m_value.object;
const auto it = object_search(members, *token);
if (!object_search_found(members, it, *token))
{
break;
}
ptr = &it->second;
}
else if (ptr->is_array())
{
if (is_dash(*token))
{
break;
}
const auto idx = array_index<BasicJsonType>(*token);
if (idx >= ptr->m_data.m_value.array->size())
{
break;
}
ptr = &(*ptr->m_data.m_value.array)[idx];
}
else if (ptr->is_null())
{
break;
}
else
{
// primitive value with a reference token left
throw_unresolved(*token, ptr);
}
}
if (token == end)
{
return *ptr;
}
}
get_unchecked_rollback<BasicJsonType> rollback(ptr);
BasicJsonType& result = create_values(ptr, token, end);
rollback.commit();
return result;
}
/*!
@brief the loop of @ref get_unchecked_create
@a ptr is a null value or an array. Every value created here is a fresh
one, so every object a key is added to is empty.
@throw out_of_range.404 if an array index is too large for strtoull
@throw out_of_range.410 if an array index exceeds size_type
@throw std::length_error if padding an array exceeds its maximal size
@throw std::bad_alloc if an allocation fails
*/
template<typename BasicJsonType>
static BasicJsonType& create_values(BasicJsonType* ptr, const string_t* first, const string_t* end)
{
for (const auto* token = first; token != end; ++token)
{
const auto& reference_token = *token;
// convert null values to arrays or objects before continuing
if (ptr->is_null())
{
// change value to an array for array indices or "-" or to an
// object otherwise; every null value here is a fresh one (see
// get_unchecked_rollback), so converting it in place leaves
// the same value as assigning an array or object to it
if (is_array_index_token(reference_token) || is_dash(reference_token))
// check if the reference token is a number
const bool nums =
std::all_of(reference_token.begin(), reference_token.end(),
[](const unsigned char x)
{
ptr->template convert_null_to<typename BasicJsonType::array_t>();
}
else
{
ptr->template convert_null_to<typename BasicJsonType::object_t>();
}
return std::isdigit(x);
});
// change value to an array for numbers or "-" or to object otherwise
*ptr = (nums || reference_token == "-")
? detail::value_t::array
: detail::value_t::object;
}
if (ptr->is_object())
{
// the object is new and empty
ptr = &object_add(*ptr, reference_token);
// use unchecked object access
ptr = &object_member(*ptr, reference_token);
}
else
else if (ptr->is_array())
{
// only the first value is not created here, and it is a null
// value or an array (see get_unchecked_create)
JSON_ASSERT(ptr->is_array());
if (is_dash(reference_token))
if (reference_token == "-")
{
// explicitly treat "-" as index beyond the end
ptr = &array_append(*ptr);
ptr = &ptr->operator[](ptr->m_data.m_value.array->size());
}
else
{
@@ -25049,31 +24601,16 @@ class json_pointer
ptr = &ptr->operator[](array_index<BasicJsonType>(reference_token));
}
}
else
{
// primitive value with a reference token left
throw_unresolved(reference_token, ptr);
}
}
return *ptr;
}
/// whether @a s is a valid array index, that is a nonempty sequence of
/// digits without a leading '0' (cf. RFC 6901, Sect. 4); tokens that could
/// never be a valid array index (such as "01" or "") are object keys
static bool is_array_index_token(const string_t& s)
{
const std::size_t size = s.size();
if (size == 0 || (size > 1 && s[0] == '0'))
{
return false;
}
for (std::size_t i = 0; i < size; ++i)
{
if (std::isdigit(static_cast<unsigned char>(s[i])) == 0)
{
return false;
}
}
return true;
}
/*!
@throw parse_error.106 if an array index begins with '0'
@throw parse_error.109 if an array index was not a number
@@ -43256,7 +42793,6 @@ public:
switch (parent.m_data.m_type)
{
case value_t::null:
case value_t::object:
{
// use operator[] to add value
@@ -43288,6 +42824,7 @@ public:
// the parent of an "add" target must be an object or array
// (see #4292)
case value_t::null:
case value_t::string:
case value_t::boolean:
case value_t::number_integer:
-17
View File
@@ -373,23 +373,6 @@ TEST_CASE("alternative string type")
CHECK_FALSE(j.contains(alt_json::json_pointer("/foo/bar")));
}
SECTION("non-const operator[](json_pointer)")
{
// creating values must compile without string_t::begin()
alt_json j = alt_json::parse(R"({"a":{"b":[1]},"c":null})");
const alt_json before = j;
j[alt_json::json_pointer("/a/b/3/x/-/y")] = 1; // pad, append, create
j[alt_json::json_pointer("/a/z")] = 2; // insert a key
j[alt_json::json_pointer("/c/0/q")] = 3; // convert null
j[alt_json::json_pointer("/a/b/-")] = 4; // append
CHECK(j.dump() == R"({"a":{"b":[1,null,null,{"x":[{"y":1}]},4],"z":2},"c":[{"q":3}]})");
// a failing access leaves the value unchanged
alt_json k = before;
CHECK_THROWS_AS(k[alt_json::json_pointer("/a/n/99999999999999999999999")], alt_json::out_of_range&);
CHECK(k == before);
}
SECTION("operator/(std::size_t)")
{
// json_pointer::operator/=(std::size_t) must compile without string_t
+223 -1
View File
@@ -15,6 +15,7 @@ using nlohmann::json;
#endif
#include <fstream>
#include <limits>
#include <string>
#include <vector>
#include "make_test_data_available.hpp"
@@ -490,7 +491,9 @@ TEST_CASE("JSON patch")
}
// A.13. Invalid JSON Patch Document
// not applicable
// not applicable: the library cannot tell the duplicate "op" members
// apart, because the parser resolves them (the last one is kept); see
// "duplicate member names in an operation" below
SECTION("A.14. Escape Ordering")
{
@@ -1515,6 +1518,24 @@ TEST_CASE("JSON patch - add to a primitive parent (regression #4292)")
#endif
}
SECTION("null parent")
{
json const doc = {{"foo", nullptr}};
json const patch = {{{"op", "add"}, {"path", "/foo/bar"}, {"value", 1}}};
#if JSON_DIAGNOSTICS
CHECK_THROWS_WITH_AS(doc.patch(patch), "[json.exception.out_of_range.411] (/foo) cannot add value: the JSON Patch 'add' target's parent is of type null, but must be an object or array", json::out_of_range&);
#else
CHECK_THROWS_WITH_AS(doc.patch(patch), "[json.exception.out_of_range.411] cannot add value: the JSON Patch 'add' target's parent is of type null, but must be an object or array", json::out_of_range&);
#endif
}
SECTION("null document")
{
json const doc = nullptr;
json const patch = {{{"op", "add"}, {"path", "/-"}, {"value", 1}}};
CHECK_THROWS_WITH_AS(doc.patch(patch), "[json.exception.out_of_range.411] cannot add value: the JSON Patch 'add' target's parent is of type null, but must be an object or array", json::out_of_range&);
}
SECTION("number parent")
{
json const doc = {{"foo", 1}};
@@ -2103,3 +2124,204 @@ TEST_CASE_TEMPLATE("JSON patch - diff round trip on hand-made documents", Json,
roundtrip(Json(1), Json::parse("[1]"));
}
}
TEST_CASE("JSON patch - behavior documented in the API reference")
{
SECTION("duplicate member names in an operation")
{
// RFC 6902, A.13: the parser resolves the duplicates (the last one
// is kept), so this is a "remove" and not an "add"
const json patch = json::parse(R"([{"op": "add", "op": "remove", "path": "/a"}])");
CHECK(patch == json::parse(R"([{"op": "remove", "path": "/a"}])"));
CHECK(json({{"a", 1}, {"b", 2}}).patch(patch) == json({{"b", 2}}));
}
SECTION("add with a leading zero in an array index")
{
const json patch = R"([{"op": "add", "path": "/01", "value": 1}])"_json;
json doc = json::array();
#if !defined(JSON_NOEXCEPTION)
CHECK_THROWS_AS(doc.patch(patch), json::parse_error&);
CHECK_THROWS_AS(doc.patch_inplace(patch), json::parse_error&);
#endif
CHECK(doc == json::array());
}
SECTION("test compares numbers by value")
{
const json doc = R"({"a": 1, "b": 0, "c": 1.5})"_json;
CHECK(doc.patch(R"([{"op": "test", "path": "/a", "value": 1.0}])"_json) == doc);
CHECK(doc.patch(R"([{"op": "test", "path": "/b", "value": -0.0}])"_json) == doc);
CHECK(doc.patch(R"([{"op": "test", "path": "/c", "value": 1.5}])"_json) == doc);
CHECK_THROWS_AS(doc.patch(R"([{"op": "test", "path": "/a", "value": 2}])"_json), json::other_error&);
CHECK_THROWS_AS(doc.patch(R"([{"op": "test", "path": "/a", "value": "1"}])"_json), json::other_error&);
}
SECTION("test ignores the order of object members, but not the order of array elements")
{
const json doc = R"({"o": {"x": 1, "y": 2}, "a": [1, 2]})"_json;
CHECK(doc.patch(R"([{"op": "test", "path": "/o", "value": {"y": 2, "x": 1}}])"_json) == doc);
CHECK(doc.patch(R"([{"op": "test", "path": "/a", "value": [1, 2]}])"_json) == doc);
CHECK_THROWS_AS(doc.patch(R"([{"op": "test", "path": "/a", "value": [2, 1]}])"_json), json::other_error&);
}
SECTION("test on a path that does not exist")
{
const json doc = R"({"a": 1})"_json;
// with a "value" member, the failed lookup is a failed test
CHECK_THROWS_AS(doc.patch(R"([{"op": "test", "path": "/b", "value": 1}])"_json), json::other_error&);
CHECK_THROWS_AS(doc.patch(R"([{"op": "test", "path": "/b", "value": null}])"_json), json::other_error&);
// without it, an exception is thrown, too; which one depends on the
// order of evaluation of the operands of ==, so it is not checked
CHECK_THROWS(doc.patch(R"([{"op": "test", "path": "/b"}])"_json));
}
SECTION("patch() has the strong guarantee")
{
const json doc = R"({"a": [1, 2], "o": {"k": "v"}})"_json;
const json before = doc;
const std::vector<json> failing =
{
// remove of a member that does not exist
R"([{"op": "add", "path": "/n", "value": 1}, {"op": "remove", "path": "/missing"}])"_json,
// replace of a member that does not exist
R"([{"op": "remove", "path": "/a/0"}, {"op": "replace", "path": "/missing", "value": 1}])"_json,
// move from a member that does not exist
R"([{"op": "add", "path": "/n", "value": 1}, {"op": "move", "from": "/missing", "path": "/m"}])"_json,
// copy from a member that does not exist
R"([{"op": "add", "path": "/n", "value": 1}, {"op": "copy", "from": "/missing", "path": "/m"}])"_json,
// out-of-range array index
R"([{"op": "add", "path": "/n", "value": 1}, {"op": "add", "path": "/a/5", "value": 1}])"_json,
// failing test after a successful operation
R"([{"op": "remove", "path": "/o"}, {"op": "test", "path": "/a", "value": []}])"_json,
};
for (const auto& patch : failing)
{
CAPTURE(patch.dump())
CHECK_THROWS_AS(doc.patch(patch), json::exception&);
CHECK(doc == before);
json copy = doc;
CHECK_THROWS_AS(copy.patch(patch), json::exception&);
CHECK(copy == before);
}
}
}
TEST_CASE("JSON patch - diff behavior documented in the API reference")
{
SECTION("path is a verbatim prefix")
{
// not escaped and not validated
CHECK(json::diff(1, 2) == R"([{"op": "replace", "path": "", "value": 2}])"_json);
CHECK(json::diff(1, 2, "abc") == R"([{"op": "replace", "path": "abc", "value": 2}])"_json);
CHECK(json::diff(1, 2, "/root") == R"([{"op": "replace", "path": "/root", "value": 2}])"_json);
// an escaped token in a non-root prefix is kept as given
CHECK(json::diff(R"({"a": 1})"_json, R"({"a": 2})"_json, "/x~1y/~0")
== R"([{"op": "replace", "path": "/x~1y/~0/a", "value": 2}])"_json);
CHECK(json::diff(R"({"a": 1})"_json, R"({"a": 2})"_json, "/p")
== R"([{"op": "replace", "path": "/p/a", "value": 2}])"_json);
}
SECTION("keys are escaped in the paths")
{
const json source = json::object();
const json target =
{
{"", 0}, {"/", 1}, {"~", 2}, {"~1", 3}, {"~01", 4}
};
const json patch = json::diff(source, target);
REQUIRE(patch.size() == 5);
// the order of the operations is not checked
json paths = json::object();
for (const auto& op : patch)
{
CHECK(op["op"] == "add");
paths[op["path"].get<std::string>()] = op["value"];
}
CHECK(paths == json({{"/", 0}, {"/~1", 1}, {"/~0", 2}, {"/~01", 3}, {"/~001", 4}}));
CHECK(source.patch(patch) == target);
}
SECTION("NaN is not equal to itself")
{
const json nan = std::numeric_limits<json::number_float_t>::quiet_NaN();
const json patch = json::diff(nan, nan);
CHECK(patch.size() == 1);
CHECK(patch[0]["op"] == "replace");
CHECK(patch[0]["path"] == "");
CHECK(patch[0]["value"].is_number_float());
CHECK(nan.patch(patch) != nan);
// a NaN in a patch is serialized as null
CHECK(json::parse(patch.dump())[0]["value"].is_null());
const json nested = {{"a", {1, nan}}};
const json nested_patch = json::diff(nested, nested);
CHECK(nested_patch.size() == 1);
CHECK(nested_patch[0]["path"] == "/a/1");
// infinity is equal to itself
const json inf = std::numeric_limits<json::number_float_t>::infinity();
CHECK(json::diff(inf, inf).empty());
}
SECTION("discarded values are not equal to themselves")
{
const json discarded(json::value_t::discarded);
const json patch = json::diff(discarded, discarded);
CHECK(patch.size() == 1);
CHECK(patch[0]["op"] == "replace");
CHECK(patch[0]["path"] == "");
CHECK(patch[0]["value"].is_discarded());
CHECK(discarded.patch(patch) != discarded);
}
SECTION("numbers that compare equal produce no operation")
{
CHECK(json::diff(json(1), json(1.0)).empty());
CHECK(json::diff(json(1.0), json(1)).empty());
CHECK(json::diff(json(1U), json(1)).empty());
CHECK(json::diff(json(1), json(1U)).empty());
CHECK(json::diff(json(0.0), json(-0.0)).empty());
// the patched value keeps the number type of the source
const json source = 1;
const json patched = source.patch(json::diff(source, json(1.0)));
CHECK(patched.is_number_integer());
CHECK(patched == json(1.0));
// different values are replaced
CHECK(json::diff(json(1), json(2.0)) == R"([{"op": "replace", "path": "", "value": 2.0}])"_json);
CHECK(json::diff(json(1), json(-1)) == R"([{"op": "replace", "path": "", "value": -1}])"_json);
CHECK(json::diff(json(1.0), json(1.5)) == R"([{"op": "replace", "path": "", "value": 1.5}])"_json);
CHECK(json::diff(json(1), json("1")) == R"([{"op": "replace", "path": "", "value": "1"}])"_json);
}
SECTION("binary values")
{
const json a = json::binary({1, 2});
const json b = json::binary({1, 3});
CHECK(json::diff(a, a).empty());
const json patch = json::diff(a, b);
CHECK(patch.size() == 1);
CHECK(patch[0]["op"] == "replace");
CHECK(patch[0]["path"] == "");
CHECK(patch[0]["value"].is_binary());
CHECK(a.patch(patch) == b);
// serialized, the binary value is an object
const json parsed = json::parse(patch.dump());
CHECK(parsed[0]["value"].is_object());
CHECK(parsed[0]["value"] == R"({"bytes": [1, 3], "subtype": null})"_json);
}
SECTION("a difference as a whole is a replace of the root")
{
CHECK(json::diff(json::array({1}), json::object()) == R"([{"op": "replace", "path": "", "value": {}}])"_json);
CHECK(json::diff(json(nullptr), json::array()) == R"([{"op": "replace", "path": "", "value": []}])"_json);
}
}
-138
View File
@@ -29,64 +29,9 @@ using nlohmann::json;
#include <map>
#include <random>
#include <sstream>
#include <stdexcept>
#include <string>
#include <vector>
namespace
{
// the non-const operator[] must throw for pointer and leave the value parsed
// from init unchanged (strong exception safety)
template<typename Json>
void check_strong_guarantee(const char* init, const std::string& pointer, const char* message)
{
CAPTURE(init)
CAPTURE(pointer)
Json j = Json::parse(init);
const Json before = Json::parse(init);
const typename Json::json_pointer ptr(pointer);
CHECK_THROWS_WITH_AS(j[ptr], message, typename Json::exception&);
CHECK(j == before);
}
// as above, for an index that fits into size_type, but not into an array
template<typename Json>
void check_strong_guarantee_length_error(const char* init, const std::string& pointer)
{
CAPTURE(init)
CAPTURE(pointer)
Json j = Json::parse(init);
const Json before = Json::parse(init);
const typename Json::json_pointer ptr(pointer);
CHECK_THROWS_AS(j[ptr], std::length_error&);
CHECK(j == before);
}
template<typename Json>
void check_strong_guarantee_cases()
{
const char* const unresolved = "[json.exception.out_of_range.404] unresolved reference token '99999999999999999999999'";
// the first change converts a null value
check_strong_guarantee<Json>("null", "/99999999999999999999999", unresolved);
check_strong_guarantee<Json>("null", "/a/99999999999999999999999", unresolved);
check_strong_guarantee<Json>(R"({"a":null})", "/a/b/-/99999999999999999999999", unresolved);
// the first change inserts a key into an object
check_strong_guarantee<Json>("{}", "/a/99999999999999999999999", unresolved);
check_strong_guarantee<Json>(R"({"a":{"b":1}})", "/a/c/d/99999999999999999999999", unresolved);
// the first change pads or appends to an array
check_strong_guarantee<Json>(R"({"a":[1]})", "/a/5/99999999999999999999999", unresolved);
check_strong_guarantee<Json>(R"({"a":[1]})", "/a/-/x/1/99999999999999999999999", unresolved);
// an index that does not fit into size_type
check_strong_guarantee<Json>("null", "/x/18446744073709551615",
"[json.exception.out_of_range.410] array index 18446744073709551615 exceeds size_type");
// an index that fits into size_type, but not into an array
const auto too_large = std::to_string((std::numeric_limits<typename Json::size_type>::max)() - 1);
check_strong_guarantee_length_error<Json>("null", "/" + too_large);
check_strong_guarantee_length_error<Json>(R"({"a":[[1]]})", "/a/0/3/" + too_large);
}
} // namespace
TEST_CASE("JSON pointers")
{
SECTION("errors")
@@ -548,89 +493,6 @@ TEST_CASE("JSON pointers")
}
}
SECTION("creating intermediate levels")
{
SECTION("tokens that are valid array indices create arrays")
{
json j;
j["/0"_json_pointer] = 1;
CHECK(j == json({1}));
json j2;
j2["/2"_json_pointer] = 1;
CHECK(j2 == json({nullptr, nullptr, 1}));
json j3;
j3["/-"_json_pointer] = 1;
CHECK(j3 == json({1}));
json j4;
j4["/foo/0/0"_json_pointer] = 1;
CHECK(j4 == json({{"foo", {{1}}}}));
}
SECTION("tokens that are no valid array indices create objects")
{
json j;
j["/one"_json_pointer] = 1;
CHECK(j == json({{"one", 1}}));
// leading '0' can never be a valid array index (RFC 6901, Sect. 4)
json j2;
j2["/01"_json_pointer] = 1;
CHECK(j2 == json({{"01", 1}}));
// the empty token is a valid object key, but no valid array index
json j3;
j3["/"_json_pointer] = 1;
CHECK(j3 == json({{"", 1}}));
}
SECTION("creating a level yields the same result as reusing it (#5357)")
{
json j;
j["/a/b/01/d"_json_pointer] = "value";
json j_init = json::object();
j_init["/a/b"_json_pointer] = json::object();
j_init["/a/b/01/d"_json_pointer] = "value";
const json expected = json::parse(R"({"a":{"b":{"01":{"d":"value"}}}})");
CHECK(j == expected);
CHECK(j_init == expected);
// unflatten uses the same key
const json flat = {{"/a/b/01/d", "value"}};
CHECK(flat.unflatten() == expected);
}
SECTION("existing arrays still reject invalid indices")
{
json j = {1, 2, 3};
CHECK_THROWS_WITH_AS(j["/01"_json_pointer],
"[json.exception.parse_error.106] parse error: array index '01' must not begin with '0'", json::parse_error&);
CHECK_THROWS_WITH_AS(j.at("/01"_json_pointer),
"[json.exception.parse_error.106] parse error: array index '01' must not begin with '0'", json::parse_error&);
}
SECTION("a failing access leaves the value unchanged")
{
check_strong_guarantee_cases<json>();
check_strong_guarantee_cases<nlohmann::ordered_json>();
}
SECTION("a successful access still creates the same values")
{
json j = {{"a", {1}}};
j["/a/3/x/-/y"_json_pointer] = 1;
CHECK(j == json::parse(R"({"a":[1,null,null,{"x":[{"y":1}]}]})"));
j["/a/-"_json_pointer] = 2;
j["/a/0"_json_pointer] = 3;
j["/b"_json_pointer] = 4;
CHECK(j == json::parse(R"({"a":[3,null,null,{"x":[{"y":1}]},2],"b":4})"));
}
}
SECTION("flatten")
{
json j =
-17
View File
@@ -1499,23 +1499,6 @@ TEST_CASE("regression tests 1")
CHECK(order.dump() == R"({"a":2,"m":{"y":1,"b":2},"new_key":3})");
}
SECTION("operator[](json_pointer) when emplace() finds a key the lookup does not")
{
// a copied fifo_map restarts its timestamps, so a new key "k" gets
// the timestamp of "x": the lookup does not find "k", but emplace()
// finds "x" and inserts nothing; the pointer then continues with the
// member of "x", as basic_json::operator[] does
const my_json orig = my_json::parse(R"({"x":1})");
const std::array<const char*, 3> pointers = {{"/k/0", "/k/a/b", "/k/-"}};
for (const char* pointer : pointers)
{
CAPTURE(pointer)
my_json j = orig;
CHECK_THROWS_AS(j[my_json::json_pointer(pointer)], my_json::out_of_range&);
CHECK(j.dump() == R"({"x":1})");
}
}
SECTION("issue #977 - Assigning between different json types")
{
foo_json lj = ns::foo{3};