From d6431a91044e0e64f270127e19178aa2413c7226 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Mon, 26 Oct 2026 17:11:25 +0100 Subject: [PATCH 01/18] Throw out_of_range.411 for JSON Patch add into a null parent (#5917) (#5918) Claude-Session: https://claude.ai/code/session_01MKJf48vnGtTE25KscmjfaW Signed-off-by: Niels Lohmann Co-authored-by: Claude Sonnet 5.5 --- docs/mkdocs/docs/api/basic_json/patch.md | 1 + .../docs/api/basic_json/patch_inplace.md | 1 + docs/mkdocs/docs/home/exceptions.md | 2 ++ include/nlohmann/json.hpp | 2 +- single_include/nlohmann/json.hpp | 2 +- tests/src/unit-json_patch.cpp | 18 ++++++++++++++++++ 6 files changed, 24 insertions(+), 2 deletions(-) diff --git a/docs/mkdocs/docs/api/basic_json/patch.md b/docs/mkdocs/docs/api/basic_json/patch.md index ed26135a9..39f601683 100644 --- a/docs/mkdocs/docs/api/basic_json/patch.md +++ b/docs/mkdocs/docs/api/basic_json/patch.md @@ -108,6 +108,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 diff --git a/docs/mkdocs/docs/api/basic_json/patch_inplace.md b/docs/mkdocs/docs/api/basic_json/patch_inplace.md index 5b0d501bb..1bcfc0067 100644 --- a/docs/mkdocs/docs/api/basic_json/patch_inplace.md +++ b/docs/mkdocs/docs/api/basic_json/patch_inplace.md @@ -106,6 +106,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 diff --git a/docs/mkdocs/docs/home/exceptions.md b/docs/mkdocs/docs/home/exceptions.md index 4b8f6edd9..0a31d2444 100644 --- a/docs/mkdocs/docs/home/exceptions.md +++ b/docs/mkdocs/docs/home/exceptions.md @@ -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 diff --git a/include/nlohmann/json.hpp b/include/nlohmann/json.hpp index 3d40682d9..c2dddd55a 100644 --- a/include/nlohmann/json.hpp +++ b/include/nlohmann/json.hpp @@ -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: diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 1beba149b..f0978d69e 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -42793,7 +42793,6 @@ public: switch (parent.m_data.m_type) { - case value_t::null: case value_t::object: { // use operator[] to add value @@ -42825,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: diff --git a/tests/src/unit-json_patch.cpp b/tests/src/unit-json_patch.cpp index 67ffee6dd..de868737f 100644 --- a/tests/src/unit-json_patch.cpp +++ b/tests/src/unit-json_patch.cpp @@ -1515,6 +1515,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}}; From 8679c011db4f660b21cb490f08a8034faae196a2 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Mon, 26 Oct 2026 17:11:36 +0100 Subject: [PATCH 02/18] Use flawfinder --exclude instead of listing directories (#5935) Flawfinder 2.0.20 has an --exclude PATTERN option. Scan `./` and exclude `*/single_include/*` so that new top-level directories are covered automatically. The scanned file set is identical to the explicit directory list. Signed-off-by: Niels Lohmann --- .github/workflows/flawfinder.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/flawfinder.yml b/.github/workflows/flawfinder.yml index 2aebc6306..0c6e5fa9c 100644 --- a/.github/workflows/flawfinder.yml +++ b/.github/workflows/flawfinder.yml @@ -43,8 +43,8 @@ jobs: - name: flawfinder_scan uses: david-a-wheeler/flawfinder@c4216b74cf2639ffa98503768bd6e4299b5440c9 # v2.0.20 with: - # single_include/ is omitted: it is generated from include/ and would report every finding twice - arguments: '--sarif ./cmake ./docs ./include ./src ./tests ./tools' + # single_include/ is generated from include/ and would report every finding twice + arguments: "--sarif --exclude '*/single_include/*' ./" output: 'flawfinder_results.sarif' - name: Upload analysis results to GitHub Security tab From c20b6eddf45651fa6bb81958fe61c7416bd5d525 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Mon, 26 Oct 2026 17:11:46 +0100 Subject: [PATCH 03/18] docs: correct JSON Pointer documentation and add tests (#5937) Fix inaccurate or missing statements about unflatten (third round-trip failure, empty token, typo), value (type_error.306 before pointer lookup), contains (pointer overload), parent_pointer and operator<< (bad_alloc), out_of_range.410 example, template parameter requirements (resize(n)), and document the URI fragment form and non-unique member names of RFC 6901. Add tests for the unflatten and value() cases. Signed-off-by: Niels Lohmann --- docs/mkdocs/docs/api/basic_json/contains.md | 11 +++- docs/mkdocs/docs/api/basic_json/unflatten.md | 15 ++++-- docs/mkdocs/docs/api/basic_json/value.md | 3 ++ .../docs/api/json_pointer/parent_pointer.md | 3 +- docs/mkdocs/docs/api/operator_ltlt.md | 3 +- docs/mkdocs/docs/features/json_pointer.md | 11 ++++ .../features/types/template_parameters.md | 1 + docs/mkdocs/docs/home/exceptions.md | 7 ++- tests/src/unit-json_pointer.cpp | 50 +++++++++++++++++++ 9 files changed, 94 insertions(+), 10 deletions(-) diff --git a/docs/mkdocs/docs/api/basic_json/contains.md b/docs/mkdocs/docs/api/basic_json/contains.md index c6e2e7d77..5ed088a55 100644 --- a/docs/mkdocs/docs/api/basic_json/contains.md +++ b/docs/mkdocs/docs/api/basic_json/contains.md @@ -61,11 +61,18 @@ Strong exception safety: if an exception occurs, the original value stays intact ## Complexity -Logarithmic in the size of the JSON object. +1. Logarithmic in the size of the JSON object. +2. Logarithmic in the size of the JSON object. +3. At most one lookup per reference token of `ptr`, each logarithmic in the size of the container at that level: + O(*n* log *m*) for *n* reference tokens and containers of size at most *m*. +4. Deleted; a call with an integral argument does not compile. ## Notes -- This method always returns `#!cpp false` when executed on a JSON type that is not an object. +- Overloads (1) and (2) always return `#!cpp false` when executed on a JSON type that is not an object. Overload (3) + also works on arrays and on primitive values: the empty JSON pointer resolves in every JSON value (so + `#!cpp json(1).contains(""_json_pointer)` returns `#!cpp true`), and `#!cpp json({1, 2}).contains("/1"_json_pointer)` + returns `#!cpp true`. - This method can be executed on any JSON value type. - Calling this function with an integer argument (for example, `#!cpp contains(0)`) does not compile: such an argument would otherwise implicitly convert to a null `#!cpp const char*` and, from there, cause undefined behavior when diff --git a/docs/mkdocs/docs/api/basic_json/unflatten.md b/docs/mkdocs/docs/api/basic_json/unflatten.md index 661ffa3e8..1ae81dd15 100644 --- a/docs/mkdocs/docs/api/basic_json/unflatten.md +++ b/docs/mkdocs/docs/api/basic_json/unflatten.md @@ -36,9 +36,9 @@ The function can throw the following exceptions: - Throws [`parse_error.109`](../../home/exceptions.md#jsonexceptionparse_error109) if an array index in a key is not a number; example: `"array index 'one' is not a number"` - Throws [`out_of_range.404`](../../home/exceptions.md#jsonexceptionout_of_range404) if a level becomes an array - (because one of its keys is `0`) and another key at that level begins with a digit but is not a valid array index - (such as `1a`), or is `-`; example: - `"unresolved reference token '-'"` + (because one of its keys is `0`) and another key at that level is empty (as in `/a/`), is `-`, or begins with a digit + but is not a valid array index (such as `1a`); examples: `"unresolved reference token '-'"`, + `"unresolved reference token ''"` ## Complexity @@ -46,7 +46,7 @@ Linear in the size of the JSON value. ## Notes -Empty objects and arrays are flattened by [`flatten()`](flatten.md) to `#!json null` values and cannot unflattened to +Empty objects and arrays are flattened by [`flatten()`](flatten.md) to `#!json null` values and cannot be unflattened to their original type. A flattened array and a flattened object whose keys are array indices are indistinguishable, because both are @@ -54,7 +54,12 @@ described by the same JSON pointers. A value is therefore restored as an array i reference token `0`, and as an object otherwise: `#!json {"2": 1}` is restored unchanged, whereas `#!json {"0": 1}` is restored as `#!json [1]`. This decision does not depend on the order in which the flattened object is iterated. -Apart from these two cases, for a JSON value `j`, the following is always true: +A third case is an object that has the key `0` next to a key that is not a valid array index: `#!json {"0": 1, "b": 2}` +is flattened to `#!json {"/0": 1, "/b": 2}`, and unflatten treats the level as an array and throws +[`parse_error.109`](../../home/exceptions.md#jsonexceptionparse_error109) because `b` is not a number. (For the keys +`-`, `1a`, or the empty key instead of `b`, it throws [`out_of_range.404`](../../home/exceptions.md#jsonexceptionout_of_range404).) + +Apart from these three cases, for a JSON value `j`, the following is always true: `#!cpp j == j.flatten().unflatten()`. ## Examples diff --git a/docs/mkdocs/docs/api/basic_json/value.md b/docs/mkdocs/docs/api/basic_json/value.md index aee09a537..5aa056c92 100644 --- a/docs/mkdocs/docs/api/basic_json/value.md +++ b/docs/mkdocs/docs/api/basic_json/value.md @@ -45,6 +45,9 @@ This is equivalent to Python's `dict.get(key, default)`. return default_value; } ``` + with one exception: if the JSON value is neither an array nor an object, the function throws + [`type_error.306`](../../home/exceptions.md#jsonexceptiontype_error306) before looking at `ptr`, even for the empty + pointer, whereas `at` returns the value itself for the empty pointer. !!! note "Differences to `at` and `operator[]`" diff --git a/docs/mkdocs/docs/api/json_pointer/parent_pointer.md b/docs/mkdocs/docs/api/json_pointer/parent_pointer.md index d37889e7e..e8627714d 100644 --- a/docs/mkdocs/docs/api/json_pointer/parent_pointer.md +++ b/docs/mkdocs/docs/api/json_pointer/parent_pointer.md @@ -12,7 +12,8 @@ Parent of this JSON pointer; in case this JSON pointer is the root, the root its ## Exception safety -No-throw guarantee: this function never throws exceptions. +Strong exception safety: if an exception occurs, the original value stays intact. The function is not `#!cpp noexcept` +and allocates a new JSON pointer, so it can throw `#!cpp std::bad_alloc`; it throws no library exceptions. ## Complexity diff --git a/docs/mkdocs/docs/api/operator_ltlt.md b/docs/mkdocs/docs/api/operator_ltlt.md index 6a821b7fb..6bcb160a2 100644 --- a/docs/mkdocs/docs/api/operator_ltlt.md +++ b/docs/mkdocs/docs/api/operator_ltlt.md @@ -37,7 +37,8 @@ the stream `o` 1. Throws [`type_error.316`](../home/exceptions.md#jsonexceptiontype_error316) if a string stored inside the JSON value is not UTF-8 encoded. Note that unlike the [`dump`](basic_json/dump.md) member functions, no `error_handler` can be set. -2. None. +2. Throws no library exceptions. As the string representation is built with [`to_string`](json_pointer/to_string.md), + `#!cpp std::bad_alloc` is possible. ## Complexity diff --git a/docs/mkdocs/docs/features/json_pointer.md b/docs/mkdocs/docs/features/json_pointer.md index 1bc5eabdb..a26454be0 100644 --- a/docs/mkdocs/docs/features/json_pointer.md +++ b/docs/mkdocs/docs/features/json_pointer.md @@ -37,6 +37,10 @@ Then every value inside the JSON document can be identified as follows: Note `/` does not identify the root (i.e., the whole document), but an object entry with empty key `""`. See [RFC 6901](https://tools.ietf.org/html/rfc6901) for more information. +[RFC 6901, section 4](https://tools.ietf.org/html/rfc6901#section-4) states that evaluation fails if an object has +non-unique member names. A `basic_json` object holds one value per key (see [`object_t`](../api/basic_json/object_t.md) +for how duplicate keys are handled while parsing), so evaluating a JSON Pointer never encounters duplicates. + ## JSON Pointer creation JSON Pointers can be created from a string: @@ -54,6 +58,13 @@ auto p = "/nested/one"_json_pointer; The escaping rules of [RFC 6901](https://tools.ietf.org/html/rfc6901) are implemented. See the [constructor documentation](../api/json_pointer/json_pointer.md) for more information. +!!! note "URI fragment representation" + + [RFC 6901, section 6](https://tools.ietf.org/html/rfc6901#section-6) also defines a URI fragment identifier + representation (`#/a%20b`). It is not supported: `#!cpp json::json_pointer("#/a")` throws + [`parse_error.107`](../home/exceptions.md#jsonexceptionparse_error107). Strip the leading `#` and + percent-decode the remainder before creating the JSON pointer. + ## Value access JSON Pointers can be used in the [`at`](../api/basic_json/at.md), [`operator[]`](../api/basic_json/operator%5B%5D.md), diff --git a/docs/mkdocs/docs/features/types/template_parameters.md b/docs/mkdocs/docs/features/types/template_parameters.md index 888590234..29329fea8 100644 --- a/docs/mkdocs/docs/features/types/template_parameters.md +++ b/docs/mkdocs/docs/features/types/template_parameters.md @@ -390,6 +390,7 @@ using array_t = ArrayType>; described in RFC 6901. Neither `find(const StringType&, size_type)`, nor `substr(pos, count)`, nor `replace(pos, count, const StringType&)` is required. - `empty()`. +- `resize(n)` (one argument) -- used by `to_string` to size the result before the reference tokens are copied into it. - `begin()` and `end()` -- used by [`operator[](const json_pointer&)`](../../api/basic_json/operator%5B%5D.md) to decide whether a reference token denotes an array index. diff --git a/docs/mkdocs/docs/home/exceptions.md b/docs/mkdocs/docs/home/exceptions.md index 0a31d2444..1b8436f91 100644 --- a/docs/mkdocs/docs/home/exceptions.md +++ b/docs/mkdocs/docs/home/exceptions.md @@ -1083,9 +1083,14 @@ a JSON pointer exceeds the range of `size_type` (e.g., on 32-bit platforms). ``` enum value out of range - array index 18446744073709551616 exceeds size_type + array index 18446744073709551615 exceeds size_type ``` + The second message is thrown, for instance, by `#!cpp json::parse("[1, 2]").at("/18446744073709551615"_json_pointer)` + on a 64-bit platform, where `size_type` is `#!cpp std::size_t` and this index is `SIZE_MAX`. An index that does not + fit into 64 bits (such as `18446744073709551616`) is not reported as 410 but as + [`out_of_range.404`](#jsonexceptionout_of_range404). + ### json.exception.out_of_range.411 A JSON Patch `add` operation cannot be applied because the target location's parent is neither an object nor an array. Per [RFC 6902](https://datatracker.ietf.org/doc/html/rfc6902), an `add` target must reference a member of an existing object or an element of an existing array; a primitive value (string, number, boolean, etc.) cannot receive a new member or element. diff --git a/tests/src/unit-json_pointer.cpp b/tests/src/unit-json_pointer.cpp index 7748a375a..111fec815 100644 --- a/tests/src/unit-json_pointer.cpp +++ b/tests/src/unit-json_pointer.cpp @@ -1044,6 +1044,56 @@ TEST_CASE("flatten of structured values") CHECK(flat_object.unflatten() == nlohmann::ordered_json::parse(R"({"b": {"1": 2}})")); } +#if !defined(JSON_NOEXCEPTION) + SECTION("an object with the key \"0\" next to a non-index key does not round-trip") + { + // the third documented case in which j != j.flatten().unflatten() + const json value = {{"a", {{"0", 1}, {"b", 2}}}}; + const json flat = value.flatten(); + CHECK(flat == json({{"/a/0", 1}, {"/a/b", 2}})); + CHECK_THROWS_WITH_AS(flat.unflatten(), + "[json.exception.parse_error.109] parse error: array index 'b' is not a number", json::parse_error&); + + // the same for other keys that are not valid array indices + CHECK_THROWS_WITH_AS(json({{"/a/0", 1}, {"/a/1a", 2}}).unflatten(), + "[json.exception.out_of_range.404] unresolved reference token '1a'", json::out_of_range&); + CHECK_THROWS_WITH_AS(json({{"/a/0", 1}, {"/a/-", 2}}).unflatten(), + "[json.exception.out_of_range.404] unresolved reference token '-'", json::out_of_range&); + } + + SECTION("unflatten with the empty token among array indices") + { + CHECK_THROWS_WITH_AS(json({{"/a/0", 1}, {"/a/", 2}}).unflatten(), + "[json.exception.out_of_range.404] unresolved reference token ''", json::out_of_range&); + CHECK_THROWS_WITH_AS(json({{"/a/0", 1}, {"/a/1", 2}, {"/a/", 3}}).unflatten(), + "[json.exception.out_of_range.404] unresolved reference token ''", json::out_of_range&); + + // without an index "0", the empty token is an ordinary object key + CHECK(json({{"/a/", 2}}).unflatten() == json({{"a", {{"", 2}}}})); + } + + SECTION("value() with the empty pointer on a primitive root") + { + // unlike at(), value() rejects primitive values before it looks at the pointer + const json number = 1; + CHECK(number.at(""_json_pointer) == 1); + CHECK_THROWS_WITH_AS(number.value(""_json_pointer, 7), + "[json.exception.type_error.306] cannot use value() with number", json::type_error&); + + const json string = "s"; + CHECK_THROWS_WITH_AS(string.value(""_json_pointer, 7), + "[json.exception.type_error.306] cannot use value() with string", json::type_error&); + + // contains() accepts the empty pointer on every value + CHECK(number.contains(""_json_pointer)); + CHECK_FALSE(number.contains("/a"_json_pointer)); + + // arrays and objects return the default for a missing location + CHECK(json::array().value(""_json_pointer, json(7)) == json::array()); + CHECK(json::array().value("/0"_json_pointer, 7) == 7); + } +#endif + SECTION("objects and arrays interleaved") { const json value = From 7a10bf25d2c61964bfe0a24ce398f1cf1c0cc11c Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Mon, 26 Oct 2026 17:11:55 +0100 Subject: [PATCH 04/18] docs: state merge_patch complexity and RFC 7396 limitations; test non-object, discarded, binary cases (#5938) Signed-off-by: Niels Lohmann --- .../mkdocs/docs/api/basic_json/merge_patch.md | 28 ++++++- docs/mkdocs/docs/features/merge_patch.md | 14 +++- tests/src/unit-merge_patch.cpp | 83 +++++++++++++++++++ 3 files changed, 120 insertions(+), 5 deletions(-) diff --git a/docs/mkdocs/docs/api/basic_json/merge_patch.md b/docs/mkdocs/docs/api/basic_json/merge_patch.md index 1fafb1ece..923070080 100644 --- a/docs/mkdocs/docs/api/basic_json/merge_patch.md +++ b/docs/mkdocs/docs/api/basic_json/merge_patch.md @@ -39,13 +39,33 @@ Basic guarantee: if an exception is thrown during the operation, the JSON value ## Complexity -Linear in the lengths of `apply_patch`. +Linear in the total number of values (including nested ones) in `apply_patch`, times the cost of a key-based object +operation (looking up, adding, or erasing a member): logarithmic for [`json`](../json.md), and linear for +[`ordered_json`](../ordered_json.md) (see [`ordered_map` complexity](../ordered_map.md#complexity)). For +`ordered_json`, patching an object that has `n` members with a patch that has `m` members therefore takes O(n·m). The +cost also includes one copy of `apply_patch` (see the notes below). ## Notes -`apply_patch` may be `#!cpp *this` itself or refer to a value contained in `#!cpp *this` (for example, a subobject -returned by `#!cpp (*this)[key]`); it is read as it was when `merge_patch()` was called, before any modification of -`#!cpp *this`. +- `apply_patch` may be `#!cpp *this` itself or refer to a value contained in `#!cpp *this` (for example, a subobject + returned by `#!cpp (*this)[key]`); it is read as it was when `merge_patch()` was called, before any modification of + `#!cpp *this`. To make this possible, the function copies `apply_patch` before it modifies `#!cpp *this`, so the cost + includes one copy of the patch. +- A merge patch uses `#!json null` to remove a member, so it cannot set a member to `#!json null`: applying + `#!json {"a":null}` to `#!json {"a":1}` gives `#!json {}`. +- Arrays are not merged: an array in the patch replaces the target value as a whole, and a `#!json null` inside an + array is kept as an element. `#!json null` members of the target that the patch does not mention are preserved. +- A `#!json null` in the patch only removes a member; it does not prevent the creation of the enclosing objects. + Applying `#!json {"a":{"b":null}}` to `#!json {}` gives `#!json {"a":{}}` (see example 15 in Appendix A of the RFC). +- If `apply_patch` is not an object, it replaces the target as a whole. In particular, a `#!json null` patch turns the + target into `#!json null`. If `apply_patch` is an object and the target is not, the target is first replaced by an + empty object, so applying `#!json {}` to `#!json 1` gives `#!json {}`. +- A merge patch cannot express every change; for example, it cannot reorder array elements or remove a specific array + element. Use [JSON Patch](../../features/json_patch.md) ([RFC 6902](https://tools.ietf.org/html/rfc6902)) for that. + [`merge_diff`](merge_diff.md) has a matching limitation, see its notes. +- A [discarded](is_discarded.md) value is treated like any other non-object value: a discarded + `apply_patch` turns the target into a discarded value, and an object patch replaces a discarded target by an object + as described above. ## Examples diff --git a/docs/mkdocs/docs/features/merge_patch.md b/docs/mkdocs/docs/features/merge_patch.md index ac732c7bf..9d87f4c50 100644 --- a/docs/mkdocs/docs/features/merge_patch.md +++ b/docs/mkdocs/docs/features/merge_patch.md @@ -2,7 +2,7 @@ The library supports JSON Merge Patch ([RFC 7396](https://tools.ietf.org/html/rfc7396)) as a patch format. The merge patch format is primarily intended for use with the HTTP PATCH method as a means of describing a set of -modifications to a target resource's content. This function applies a merge patch to the current JSON value. +modifications to a target resource's content. Applying a merge patch changes the JSON value the patch is applied to. Instead of using [JSON Pointer](json_pointer.md) to specify values to be manipulated, it describes the changes using a syntax that closely mimics the document being modified. Unlike [JSON Patch](json_patch.md), a JSON Merge Patch cannot @@ -19,6 +19,18 @@ another. [`update`](../api/basic_json/update.md) with `merge_objects` set to `#!cpp true`; see [Merging objects](modifying_values.md#merging-objects). +!!! note "Limitations of RFC 7396" + + - A patch cannot set a member to `#!json null`, because `#!json null` removes the member: + `#!json {"a":1}` patched with `#!json {"a":null}` gives `#!json {}`. + - Arrays are replaced as a whole, never merged, and a `#!json null` inside an array is kept. + - `#!json null` members of the target that the patch does not mention are preserved. + - A `#!json null` that would remove a member of a not yet existing object still creates that object: + `#!json {}` patched with `#!json {"a":{"b":null}}` gives `#!json {"a":{}}`. + - A patch that is not an object replaces the whole target (a `#!json null` patch makes the target + `#!json null`); an object patch applied to a non-object target starts from an empty object. + - [`merge_diff`](../api/basic_json/merge_diff.md#notes) has a matching limitation for `#!json null` members. + ??? example The following code shows how a JSON Merge Patch is applied to a JSON document. diff --git a/tests/src/unit-merge_patch.cpp b/tests/src/unit-merge_patch.cpp index 927eb8773..3f5d79b3c 100644 --- a/tests/src/unit-merge_patch.cpp +++ b/tests/src/unit-merge_patch.cpp @@ -423,3 +423,86 @@ TEST_CASE("merge_patch() with an argument that aliases *this (#5641)") } } } + +TEST_CASE("JSON Merge Patch on non-object, discarded, and binary values") +{ + SECTION("an empty object patch turns a non-object target into an empty object") + { + json target = 1; + target.merge_patch(json::object()); + CHECK(target == json::object()); + + json from_null; + from_null.merge_patch(json::object()); + CHECK(from_null == json::object()); + } + + SECTION("a non-object patch replaces a deeply nested target") + { + // checked by type, since == recurses + SECTION("number") + { + json target = json::parse(nested_objects(1000, 0)); + target.merge_patch(json(42)); + CHECK(target.is_number_integer()); + CHECK(target.get() == 42); + } + + SECTION("array") + { + json target = json::parse(nested_objects(1000, 0)); + target.merge_patch(json({1, nullptr})); + CHECK(target.is_array()); + CHECK(target.size() == 2); + CHECK(target[1].is_null()); + } + + SECTION("null") + { + json target = json::parse(nested_objects(1000, 0)); + target.merge_patch(json(nullptr)); + CHECK(target.is_null()); + } + } + + SECTION("discarded values") + { + const json discarded(json::value_t::discarded); + + // a discarded patch is not an object and replaces the target + json target = {{"a", 1}}; + target.merge_patch(discarded); + CHECK(target.is_discarded()); + + // a discarded target is replaced by an empty object first + json discarded_target(json::value_t::discarded); + discarded_target.merge_patch(json({{"a", 1}})); + CHECK(discarded_target == json({{"a", 1}})); + + json both(json::value_t::discarded); + both.merge_patch(discarded); + CHECK(both.is_discarded()); + } + + SECTION("binary values") + { + const json bin = json::binary({1, 2, 3}, 7); + + // a binary patch is not an object and replaces the target + json target = {{"a", 1}}; + target.merge_patch(bin); + CHECK(target.is_binary()); + CHECK(target.get_binary() == bin.get_binary()); + + // a binary target is replaced by an object + json binary_target = bin; + binary_target.merge_patch(json({{"a", 1}})); + CHECK(binary_target == json({{"a", 1}})); + + // binary values nested in the patch are kept + json nested = json::object(); + nested.merge_patch(json({{"a", bin}})); + CHECK(nested["a"].is_binary()); + CHECK(nested["a"].get_binary() == bin.get_binary()); + } +} From c712df0637007b32dc9f4b30f6319491f14f879b Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Mon, 26 Oct 2026 17:12:05 +0100 Subject: [PATCH 05/18] docs: make the diff() and patch() documentation accurate; add tests (#5939) * Throw out_of_range.411 for JSON Patch add into a null parent (#5917) Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01MKJf48vnGtTE25KscmjfaW Signed-off-by: Niels Lohmann * 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 --------- Signed-off-by: Niels Lohmann Signed-off-by: Niels Lohmann Co-authored-by: Claude Sonnet 5.5 --- docs/mkdocs/docs/api/basic_json/diff.md | 45 +++- docs/mkdocs/docs/api/basic_json/patch.md | 11 +- .../docs/api/basic_json/patch_inplace.md | 11 +- docs/mkdocs/docs/features/json_patch.md | 19 +- tests/src/unit-json_patch.cpp | 206 +++++++++++++++++- 5 files changed, 282 insertions(+), 10 deletions(-) diff --git a/docs/mkdocs/docs/api/basic_json/diff.md b/docs/mkdocs/docs/api/basic_json/diff.md index c4e710a7d..d45a9a094 100644 --- a/docs/mkdocs/docs/api/basic_json/diff.md +++ b/docs/mkdocs/docs/api/basic_json/diff.md @@ -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 diff --git a/docs/mkdocs/docs/api/basic_json/patch.md b/docs/mkdocs/docs/api/basic_json/patch.md index 39f601683..1166c9a71 100644 --- a/docs/mkdocs/docs/api/basic_json/patch.md +++ b/docs/mkdocs/docs/api/basic_json/patch.md @@ -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" diff --git a/docs/mkdocs/docs/api/basic_json/patch_inplace.md b/docs/mkdocs/docs/api/basic_json/patch_inplace.md index 1bcfc0067..571ca9c12 100644 --- a/docs/mkdocs/docs/api/basic_json/patch_inplace.md +++ b/docs/mkdocs/docs/api/basic_json/patch_inplace.md @@ -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" diff --git a/docs/mkdocs/docs/features/json_patch.md b/docs/mkdocs/docs/features/json_patch.md index 878f0084c..d35ea55b0 100644 --- a/docs/mkdocs/docs/features/json_patch.md +++ b/docs/mkdocs/docs/features/json_patch.md @@ -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. diff --git a/tests/src/unit-json_patch.cpp b/tests/src/unit-json_patch.cpp index de868737f..b6c78532d 100644 --- a/tests/src/unit-json_patch.cpp +++ b/tests/src/unit-json_patch.cpp @@ -15,6 +15,7 @@ using nlohmann::json; #endif #include +#include #include #include #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") { @@ -2121,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 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()] = 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::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::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); + } +} From 81d66291c5569a5ebc92f6e163e44b22f1f97499 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Mon, 26 Oct 2026 17:13:48 +0100 Subject: [PATCH 06/18] Create objects for pointer tokens that cannot be array indices (#5358) * create objects for tokens that cannot be array indices (#5357) When operator[](json_pointer) traverses a level that does not exist yet, the null value is turned into an array or an object depending on the reference token. The check only tested whether all characters are digits, so tokens that can never be a valid array index selected an array and then failed: - "01" (and any other token with a leading '0') threw parse_error.106 - the empty token threw out_of_range.404 Both tokens are valid object keys, and both work when the level already exists as an object, so creating the level changed the outcome. Test the token against the RFC 6901, Sect. 4 grammar for array indices instead, so that such tokens create an object. This only affects pointers that threw before. Signed-off-by: Niels Lohmann * Clarify that the token 0 is a valid array index in operator[] docs The note described valid array indices as "a nonempty sequence of digits without a leading 0", which read literally excludes the token "0" itself, contradicting both the code and the /foo/0/0/0 example in the next sentence. Spell out the RFC 6901 grammar instead. Signed-off-by: Niels Lohmann --------- Signed-off-by: Niels Lohmann --- docs/mkdocs/docs/api/basic_json/operator[].md | 11 ++-- include/nlohmann/detail/json_pointer.hpp | 14 ++-- single_include/nlohmann/json.hpp | 14 ++-- tests/src/unit-json_pointer.cpp | 66 +++++++++++++++++++ 4 files changed, 91 insertions(+), 14 deletions(-) diff --git a/docs/mkdocs/docs/api/basic_json/operator[].md b/docs/mkdocs/docs/api/basic_json/operator[].md index 4356969a9..a4872dc39 100644 --- a/docs/mkdocs/docs/api/basic_json/operator[].md +++ b/docs/mkdocs/docs/api/basic_json/operator[].md @@ -137,10 +137,13 @@ 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 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). + 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 "Deprecation" diff --git a/include/nlohmann/detail/json_pointer.hpp b/include/nlohmann/detail/json_pointer.hpp index 4560714ab..457985480 100644 --- a/include/nlohmann/detail/json_pointer.hpp +++ b/include/nlohmann/detail/json_pointer.hpp @@ -984,15 +984,19 @@ class json_pointer // convert null values to arrays or objects before continuing if (ptr->is_null()) { - // check if the reference token is a number - const bool nums = - std::all_of(reference_token.begin(), reference_token.end(), - [](const unsigned char x) + // check if the reference token 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 treated as object keys + const bool nums = !reference_token.empty() + && (reference_token.size() == 1 || reference_token[0] != '0') + && std::all_of(reference_token.begin(), reference_token.end(), + [](const unsigned char x) { return std::isdigit(x); }); - // change value to an array for numbers or "-" or to object otherwise + // change value to an array for array indices or "-" or to object otherwise *ptr = (nums || reference_token == "-") ? detail::value_t::array : detail::value_t::object; diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index f0978d69e..f0b788c9e 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -24569,15 +24569,19 @@ class json_pointer // convert null values to arrays or objects before continuing if (ptr->is_null()) { - // check if the reference token is a number - const bool nums = - std::all_of(reference_token.begin(), reference_token.end(), - [](const unsigned char x) + // check if the reference token 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 treated as object keys + const bool nums = !reference_token.empty() + && (reference_token.size() == 1 || reference_token[0] != '0') + && std::all_of(reference_token.begin(), reference_token.end(), + [](const unsigned char x) { return std::isdigit(x); }); - // change value to an array for numbers or "-" or to object otherwise + // change value to an array for array indices or "-" or to object otherwise *ptr = (nums || reference_token == "-") ? detail::value_t::array : detail::value_t::object; diff --git a/tests/src/unit-json_pointer.cpp b/tests/src/unit-json_pointer.cpp index 111fec815..1a4e726ed 100644 --- a/tests/src/unit-json_pointer.cpp +++ b/tests/src/unit-json_pointer.cpp @@ -493,6 +493,72 @@ 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("flatten") { json j = From d5da84249f47ae8086ba28d71e8c09e6724d40cd Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Mon, 26 Oct 2026 18:25:44 +0100 Subject: [PATCH 07/18] Skip CBOR tags in front of map keys, validate indefinite text chunks, fix CBOR docs (#5927) * Skip CBOR tags in front of map keys and validate indefinite text chunks A tag in front of a text string map key was rejected with parse_error.113 even with cbor_tag_handler_t::ignore or store, although tags in front of values are handled. The tags are now skipped iteratively before the key is read; the handler error still rejects them. The chunks of an indefinite-length text string were concatenated before the single UTF-8 check, so a code point split over two chunks, or an invalid chunk, was accepted. RFC 8949, Section 3.2.3 requires every chunk to be valid on its own. With error_handler_t::strict, replace and ignore each chunk is now checked; keep is unchanged. The chunks are appended with append(ptr, len), so StringType needs nothing new. Signed-off-by: Niels Lohmann * docs: correct and extend the CBOR documentation Fix the negative-integer rows (they used MessagePack boundaries), the number_integer range of the 0x1B row, and the half-precision entry in the list of unused types; correct the NaN history (0xF9 since 3.8.0) and the tagged-item wording (only the tag directly in front of a byte string is stored, and only in the 0xD8..0xDB form). Document that to_cbor is not RFC 8949 deterministic, that duplicate keys are accepted (last wins), tags in front of keys, per-chunk UTF-8 validation, the exceptions parse_error.113, out_of_range.406/408 of from_cbor, the pointer and C array input traps, the partially written output on exceptions, and the store/ignore results in the binary values example. Drop the claim that the CBOR reader needs StringType::append(const StringType&) and BinaryType::insert(pos, first, last). Signed-off-by: Niels Lohmann * Regenerate single_include and apply astyle Result of make amalgamate (including make pretty) for the preceding changes. Signed-off-by: Niels Lohmann * Address review of the CBOR changes Cite RFC 8949 Section 5.3.1 for duplicate keys, describe the out_of_range.408 condition precisely, rewrap a long line, and update the stale get_string comment (per-chunk UTF-8 check). single_include regenerated. Signed-off-by: Niels Lohmann * docs: drop discarded-value remarks Remove the mention that a discarded value cannot be serialized to CBOR (type_error.321) from the exception-safety and mapping sections. The integer table and all added prose already follow the table-format and Oxford-comma rules. Signed-off-by: Niels Lohmann --------- Signed-off-by: Niels Lohmann --- .../docs/api/basic_json/cbor_tag_handler_t.md | 6 +- docs/mkdocs/docs/api/basic_json/from_cbor.md | 16 +- docs/mkdocs/docs/api/basic_json/to_cbor.md | 3 +- .../docs/features/binary_formats/cbor.md | 56 +++++-- docs/mkdocs/docs/features/binary_values.md | 11 ++ .../features/types/template_parameters.md | 6 +- .../nlohmann/detail/input/binary_reader.hpp | 86 ++++++++-- single_include/nlohmann/json.hpp | 86 ++++++++-- tests/src/unit-cbor.cpp | 151 +++++++++++++++++- 9 files changed, 372 insertions(+), 49 deletions(-) diff --git a/docs/mkdocs/docs/api/basic_json/cbor_tag_handler_t.md b/docs/mkdocs/docs/api/basic_json/cbor_tag_handler_t.md index 55a7954d5..528fb199e 100644 --- a/docs/mkdocs/docs/api/basic_json/cbor_tag_handler_t.md +++ b/docs/mkdocs/docs/api/basic_json/cbor_tag_handler_t.md @@ -15,11 +15,13 @@ error : report a parse error in case of a tag (the `from_cbor` overloads throw a `parse_error` exception by default) ignore -: ignore tags +: ignore tags (also in front of a text string map key) store : store tagged byte strings (for bytes 0xd8..0xdb) as binary values with the tag as subtype; other tagged values are - read as if the tag were ignored. If several tags precede a byte string, only the innermost one is stored. + read as if the tag were ignored. If several tags precede a byte string, only the tag directly in front of it is + stored, and only when it has the form 0xd8..0xdb (a tag 0..23 written as 0xc0..0xd7 is skipped). Tags in front of a + text string map key are skipped. ## Examples diff --git a/docs/mkdocs/docs/api/basic_json/from_cbor.md b/docs/mkdocs/docs/api/basic_json/from_cbor.md index 4fcd7fe98..1c14d550d 100644 --- a/docs/mkdocs/docs/api/basic_json/from_cbor.md +++ b/docs/mkdocs/docs/api/basic_json/from_cbor.md @@ -32,8 +32,10 @@ The exact mapping and its limitations are described on a [dedicated page](../../ - an `std::istream` object - a `FILE` pointer - - a C-style array of characters - - a pointer to a null-terminated string of single byte characters + - a C-style array of characters (its terminating NUL is read as trailing data: `parse_error.110` unless `strict` is + `false`) + - a pointer to a null-terminated string of single byte characters (read up to the first 0x00 byte, which is a valid + CBOR byte) - a container `obj` for which `begin(obj)` and `end(obj)` produce a valid pair of iterators (as found via ADL or member functions, with semantics compatible to `std::begin` and `std::end`) @@ -89,8 +91,14 @@ Strong guarantee: if an exception is thrown, there are no changes in the JSON va - Throws [parse_error.112](../../home/exceptions.md#jsonexceptionparse_error112) if unsupported features from CBOR were used in the given input or if the input is not valid CBOR - Throws [parse_error.113](../../home/exceptions.md#jsonexceptionparse_error113) if a map key is not a string (keys of - other types are not supported, as JSON object keys are always strings), or if a string value or object key is not - valid UTF-8 and `error_handler` is `strict` + other types are not supported, as JSON object keys are always strings), if the head of a string or byte string is + malformed (including a chunk of the wrong type or an indefinite-length string nested inside an indefinite-length + string), or if a string value or object key (or one chunk of an indefinite-length string) is not valid UTF-8 and + `error_handler` is `strict` +- Throws [out_of_range.408](../../home/exceptions.md#jsonexceptionout_of_range408) if an array or map announces + a length that `std::size_t` cannot hold, or that equals `std::size_t(-1)` (for example `9B FF FF FF FF FF FF FF FF`) +- Throws [out_of_range.406](../../home/exceptions.md#jsonexceptionout_of_range406) if a floating-point number does not + fit into `number_float_t` ## Complexity diff --git a/docs/mkdocs/docs/api/basic_json/to_cbor.md b/docs/mkdocs/docs/api/basic_json/to_cbor.md index 6508864e1..f0be593f0 100644 --- a/docs/mkdocs/docs/api/basic_json/to_cbor.md +++ b/docs/mkdocs/docs/api/basic_json/to_cbor.md @@ -42,7 +42,8 @@ The exact mapping and its limitations are described on a [dedicated page](../../ ## Exception safety -Strong guarantee: if an exception is thrown, there are no changes in the JSON value. +Strong guarantee: if an exception is thrown, there are no changes in the JSON value. Bytes that were already written to +the output adapter are not taken back. ## Exceptions diff --git a/docs/mkdocs/docs/features/binary_formats/cbor.md b/docs/mkdocs/docs/features/binary_formats/cbor.md index 8933630da..35d72d6a1 100644 --- a/docs/mkdocs/docs/features/binary_formats/cbor.md +++ b/docs/mkdocs/docs/features/binary_formats/cbor.md @@ -11,7 +11,7 @@ extremely small code sizes, fairly small message size, and extensibility without ## Byte layout -CBOR encodes every data item as a *head* followed by optional content. The first byte of the head holds the *major type* in its upper three bits (unsigned integer, negative integer, byte string, text string, array, map, tag, or simple/float) and an *additional information* value in its lower five bits. Additional information below 24 is the argument itself (for example, a small integer or a short length); the values 24 to 27 announce that the argument follows in 1, 2, 4, or 8 big-endian bytes. Strings, arrays, and maps are prefixed with their length (or count of elements/pairs), and content follows directly, so a reader always knows how many items to expect without a terminator. +CBOR encodes every data item as a *head* followed by optional content. The first byte of the head holds the *major type* in its upper three bits (unsigned integer, negative integer, byte string, text string, array, map, tag, or simple/float) and an *additional information* value in its lower five bits. Additional information below 24 is the argument itself (for example, a small integer or a short length); the values 24 to 27 announce that the argument follows in 1, 2, 4, or 8 big-endian bytes. Strings, arrays, and maps are prefixed with their length (or count of elements/pairs), and content follows directly. A reader of definite-length items knows how many items to expect without a terminator; indefinite-length items (additional information 31) are instead closed by the "break" byte 0xFF, which `from_cbor` reads but `to_cbor` never writes. Consider the following JSON value (shown formatted for readability): @@ -52,16 +52,16 @@ The library uses the following mapping from JSON values types to CBOR types acco | null | `null` | Null | 0xF6 | | boolean | `true` | True | 0xF5 | | boolean | `false` | False | 0xF4 | -| number_integer | -9223372036854775808..-2147483649 | Negative integer (8 bytes follow) | 0x3B | -| number_integer | -2147483648..-32769 | Negative integer (4 bytes follow) | 0x3A | -| number_integer | -32768..-129 | Negative integer (2 bytes follow) | 0x39 | -| number_integer | -128..-25 | Negative integer (1 byte follow) | 0x38 | +| number_integer | -9223372036854775808..-4294967297 | Negative integer (8 bytes follow) | 0x3B | +| number_integer | -4294967296..-65537 | Negative integer (4 bytes follow) | 0x3A | +| number_integer | -65536..-257 | Negative integer (2 bytes follow) | 0x39 | +| number_integer | -256..-25 | Negative integer (1 byte follow) | 0x38 | | number_integer | -24..-1 | Negative integer | 0x20..0x37 | | number_integer | 0..23 | Integer | 0x00..0x17 | | number_integer | 24..255 | Unsigned integer (1 byte follow) | 0x18 | | number_integer | 256..65535 | Unsigned integer (2 bytes follow) | 0x19 | | number_integer | 65536..4294967295 | Unsigned integer (4 bytes follow) | 0x1A | -| number_integer | 4294967296..18446744073709551615 | Unsigned integer (8 bytes follow) | 0x1B | +| number_integer | 4294967296..9223372036854775807 | Unsigned integer (8 bytes follow) | 0x1B | | number_unsigned | 0..23 | Integer | 0x00..0x17 | | number_unsigned | 24..255 | Unsigned integer (1 byte follow) | 0x18 | | number_unsigned | 256..65535 | Unsigned integer (2 bytes follow) | 0x19 | @@ -95,7 +95,24 @@ see "binary" cells in the table above. !!! success "Complete mapping" - The mapping is **complete** in the sense that any JSON value type can be converted to a CBOR value. + The mapping is **complete** in the sense that any JSON value type can be converted to a CBOR value, with one + exception: an integer outside the range of CBOR integers (only possible with a number type wider than 64 bits) + throws [`out_of_range.407`](../../home/exceptions.md#jsonexceptionout_of_range407). + +!!! info "Not a deterministic encoding" + + `to_cbor` does not produce the deterministic encoding of + [RFC 8949, Section 4.2.1](https://www.rfc-editor.org/rfc/rfc8949.html#section-4.2.1), and not always the preferred + serialization of Section 4.1: + + - floating-point numbers are written as single precision (0xFA) when that is exact and as double precision (0xFB) + otherwise; half precision (0xF9) is used only for `NaN` and `Infinity`/`-Infinity` (`1.0` is `fa 3f 80 00 00`); + - a binary subtype 0..23 is written as `d8 xx` instead of the one-byte tag 0xC0..0xD7 (`d8 01 41 01`); + - object members are written in the order of `object_t` (for the default `std::map`: sorted by the key string, not + by the encoded key bytes). + + Integers and lengths use the shortest form, and indefinite-length items are never written. Decoders must accept all + of this; the RFC does not require the preferred serialization. !!! info "NaN/infinity handling" @@ -105,9 +122,8 @@ see "binary" cells in the table above. !!! note - Prior to version 3.13.0, NaN and Infinity were instead serialized as a CBOR double-precision float (type 0xFB, - 9 bytes total), because the check used to select a smaller encoding compared magnitudes with NaN, which is - always `false` and caused the intended half-precision path to be skipped. + Prior to version 3.8.0, NaN and Infinity were instead serialized as a CBOR double-precision float (type 0xFB, + 9 bytes total). !!! info "Unused CBOR types" @@ -124,7 +140,6 @@ see "binary" cells in the table above. - expected conversions (0xD5..0xD7) - simple values (0xE0..0xF3, 0xF8) - undefined (0xF7) - - half-precision floats (0xF9) - break (0xFF) !!! info "Tagged items" @@ -226,10 +241,20 @@ string of the same major type, as required by [RFC 8949, Section 3.2.3](https:// ([RFC 8392](https://www.rfc-editor.org/rfc/rfc8392.html)), cannot be read with this library and need a general-purpose CBOR library instead. + A tag in front of a text string key is handled like a tag in front of a value: it is skipped with + `cbor_tag_handler_t::ignore` or `cbor_tag_handler_t::store` and rejected with the default `error`. A tag does not + turn a key of another type into a text string. + + A map with duplicate keys is well-formed, but invalid under + [RFC 8949, Section 5.3.1](https://www.rfc-editor.org/rfc/rfc8949.html#section-5.3.1). `from_cbor` accepts it and the + last value wins; the [SAX interface](../parsing/sax_interface.md) reports every occurrence. + !!! warning "Ill-formed UTF-8 in text strings" [RFC 8949, Section 3.1](https://www.rfc-editor.org/rfc/rfc8949.html#section-3.1) requires CBOR text strings (major - type 3) to be valid UTF-8, but leaves it up to the decoder whether to enforce this, so checking is opt-in: with the + type 3) to be valid UTF-8 (a string with ill-formed UTF-8 is "well-formed but invalid"), and + [Section 5.3.1](https://www.rfc-editor.org/rfc/rfc8949.html#section-5.3.1) leaves it up to the decoder whether to + verify this, so checking is opt-in: with the [`error_handler`](../../api/basic_json/from_cbor.md) parameter left at `keep` (the default), `from_cbor()` accepts a text string (object keys included) whose bytes are not valid UTF-8 and hands them back unchanged. Passing `error_handler_t::strict` makes `from_cbor()` check and throw @@ -242,9 +267,14 @@ string of the same major type, as required by [RFC 8949, Section 3.2.3](https:// [`JSON_STRICT_BINARY_UTF8`](../../api/macros/json_strict_binary_utf8.md) is enabled), it throws the same exception instead. Byte strings (major type 2) are unaffected, since they are not required to hold text. + The chunks of an indefinite-length text string are checked one by one, as required by + [RFC 8949, Section 3.2.3](https://www.rfc-editor.org/rfc/rfc8949.html#section-3.2.3): a code point cannot be spread + over two chunks. With `error_handler_t::strict` such a string throws `parse_error.113`; `replace` and `ignore` + sanitize each chunk on its own. + !!! warning "Tagged items" - Tagged items (0xC0..0xDB) will throw a parse error by default. They can be ignored by passing `cbor_tag_handler_t::ignore` to function `from_cbor`, in which case the tag is skipped and the enclosed data item is parsed on its own. Passing `cbor_tag_handler_t::store` to function `from_cbor` stores tagged byte strings (for bytes 0xd8..0xdb) as binary values with the tag as subtype; other tagged values are read as if the tag were ignored. If several tags precede a byte string, only the innermost one is stored. Note that no tag is ever interpreted: for instance, a text string tagged with tag 0 (date/time) stays a string. + Tagged items (0xC0..0xDB) will throw a parse error by default. They can be ignored by passing `cbor_tag_handler_t::ignore` to function `from_cbor`, in which case the tag is skipped and the enclosed data item is parsed on its own. Passing `cbor_tag_handler_t::store` to function `from_cbor` stores tagged byte strings (for bytes 0xd8..0xdb) as binary values with the tag as subtype; other tagged values are read as if the tag were ignored. If several tags precede a byte string, only the tag directly in front of it is stored, and only when it has the form 0xD8..0xDB (a tag 0..23 written as 0xC0..0xD7 is skipped); for example, `D8 05 C1 41 AA` yields subtype `null`, whereas `C1 D8 05 41 AA` yields 5. Tags in front of a text string map key are skipped as well. Note that no tag is ever interpreted: for instance, a text string tagged with tag 0 (date/time) stays a string. ??? example "Example: deserialize a JSON value from CBOR" diff --git a/docs/mkdocs/docs/features/binary_values.md b/docs/mkdocs/docs/features/binary_values.md index b4c3bab6d..dd683f469 100644 --- a/docs/mkdocs/docs/features/binary_values.md +++ b/docs/mkdocs/docs/features/binary_values.md @@ -301,6 +301,8 @@ representation using the length of the byte array. [`json::from_cbor`](../api/basic_json/from_cbor.md) (see [`cbor_tag_handler_t`](../api/basic_json/cbor_tag_handler_t.md)). + With `ignore`, the subtype is dropped (`"subtype": null`); with `store`, the tag is kept (`"subtype": 42`): + ```json { "binary": { @@ -310,6 +312,15 @@ representation using the length of the byte array. } ``` + ```json + { + "binary": { + "bytes": [202, 254, 186, 190], + "subtype": 42 + } + } + ``` + ### MessagePack [MessagePack](binary_formats/messagepack.md) supports binary values and subtypes. If a subtype is given, the ext family diff --git a/docs/mkdocs/docs/features/types/template_parameters.md b/docs/mkdocs/docs/features/types/template_parameters.md index 29329fea8..cc079cd72 100644 --- a/docs/mkdocs/docs/features/types/template_parameters.md +++ b/docs/mkdocs/docs/features/types/template_parameters.md @@ -368,8 +368,7 @@ using array_t = ArrayType>; `#!cpp std::from_chars` is not available or declines the token, and `std::strtold` reads up to the null character. A type whose `data()` is not null-terminated does not fail to compile; with such a `NumberFloatType` it can silently misparse floating-point numbers. -- `append(const char*, size_type)`, used by [`dump`](../../api/basic_json/dump.md), and `append(const StringType&)`, - used by the CBOR reader for indefinite-length strings. The library's internal string concatenation additionally has +- `append(const char*, size_type)`, used by [`dump`](../../api/basic_json/dump.md) and by the binary readers. The library's internal string concatenation additionally has to append a `#!cpp char` and a `#!cpp const char*`; for each it selects between `append(arg)`, `#!cpp operator+=`, `append(first, last)`, and `append(data, size)`. - The comparison operator `==` against another `StringType`, and `<` for use as a key of the chosen @@ -679,8 +678,7 @@ using binary_t = nlohmann::byte_container_with_subtype; [`StringType`](#stringtype), whose non-`#!cpp const` `data()` is C++17 and later only. - Default-constructible, copy-constructible, and move-constructible. - Member functions `size()`, `empty()`, `data()`, `resize()`, `operator[]`, `back()`, `begin()`, `end()`, `cbegin()`, - and `cend()` with random-access iterators, and `insert(pos, first, last)`, which the CBOR reader uses to join the - chunks of an indefinite-length byte string. `push_back()` is **not** required. + and `cend()` with random-access iterators. `push_back()` is **not** required. - Comparison operators: `==` is used by [`byte_container_with_subtype`](../../api/byte_container_with_subtype/index.md), the relational operators by [`basic_json`'s comparison operators](../../api/basic_json/operator_le.md). diff --git a/include/nlohmann/detail/input/binary_reader.hpp b/include/nlohmann/detail/input/binary_reader.hpp index d3c3d5bdc..929ac7573 100644 --- a/include/nlohmann/detail/input/binary_reader.hpp +++ b/include/nlohmann/detail/input/binary_reader.hpp @@ -1218,6 +1218,10 @@ class binary_reader // level as required by RFC 8949, Section 3.2.3 bool indefinite = false; + // the chunk currently being validated; only used for the chunks of an + // indefinite-length string when the UTF-8 check can do something + string_t chunk; + while (true) { if (JSON_HEDLEY_UNLIKELY(!unexpect_eof("string"))) @@ -1237,26 +1241,82 @@ class binary_reader } // a break marker closes the indefinite-length string; outside - // of one it falls through to the error below + // of one it falls through to the error below. Every chunk has + // been checked on its own, so there is nothing left to check. if (indefinite && current == 0xFF) { - return check_string_utf8(result, context); - } - - if (JSON_HEDLEY_UNLIKELY(!get_cbor_string_chunk(result, indefinite))) - { - return false; + return true; } if (!indefinite) { - return check_string_utf8(result, context); + return get_cbor_string_chunk(result, false) && check_string_utf8(result, context); + } + + if (error_handler == error_handler_t::keep) + { + // nothing to check: read the chunk directly into the result + if (JSON_HEDLEY_UNLIKELY(!get_cbor_string_chunk(result, true))) + { + return false; + } + } + else + { + // RFC 8949, Section 3.2.3: each chunk is a text string of its + // own and must be valid UTF-8 by itself, i.e., a code point + // cannot be spread over two chunks + chunk.clear(); + if (JSON_HEDLEY_UNLIKELY(!get_cbor_string_chunk(chunk, true) || !check_string_utf8(chunk, context))) + { + return false; + } + result.append(chunk.data(), chunk.size()); } get(); } } + /*! + @brief skips the tags in front of a CBOR object key + + A tag may wrap any data item, so it may also wrap a key. Like in front of a + value, the tags are skipped for the handlers ignore and store; with the + handler error they are left to @ref get_cbor_object_key, which rejects the + key. The tag loop is iterative, so the number of tags is not limited by the + recursion depth. + + @param[in] tag_handler how CBOR tags should be treated + + @return whether the tags could be skipped (false on EOF in a tag argument) + + @pre @a current is the first byte of the key + */ + bool skip_cbor_key_tags(const cbor_tag_handler_t tag_handler) + { + if (tag_handler == cbor_tag_handler_t::error) + { + return true; + } + + // 0xC0..0xDB: tag with the tag number in the head (0xC0..0xD7) or in + // the 1, 2, 4, or 8 bytes that follow (0xD8..0xDB) + while (current >= 0xC0 && current <= 0xDB) + { + if (current >= 0xD8) + { + std::uint64_t tag_to_ignore{}; + if (JSON_HEDLEY_UNLIKELY(!get_cbor_argument(tag_to_ignore))) + { + return false; + } + } + get(); + } + return true; + } + /*! @brief reads a CBOR object key @@ -1623,6 +1683,10 @@ class binary_reader if (top.is_object) { + if (JSON_HEDLEY_UNLIKELY(!skip_cbor_key_tags(tag_handler))) + { + return false; + } if (JSON_HEDLEY_UNLIKELY(!read_object_key<&binary_reader::get_cbor_object_key>(key))) { return false; @@ -4130,9 +4194,9 @@ class binary_reader // sequence), UBJSON, BJData, or BSON requires a decoder to reject // ill-formed UTF-8. Checking (and, with @ref error_handler_t::strict, // rejecting, or with `replace`/`ignore`, sanitizing) is opt-in via - // @ref error_handler, applied once the whole string (all chunks of - // an indefinite-length CBOR string included) has been assembled, by - // @ref check_string_utf8 at the call site. + // @ref error_handler, applied by @ref check_string_utf8 at the call + // site to the whole string or, for an indefinite-length CBOR string, + // to each chunk on its own (RFC 8949, Section 3.2.3). return get_bytes(len, "string", result); } diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index f0b788c9e..5db064c63 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -18685,6 +18685,10 @@ class binary_reader // level as required by RFC 8949, Section 3.2.3 bool indefinite = false; + // the chunk currently being validated; only used for the chunks of an + // indefinite-length string when the UTF-8 check can do something + string_t chunk; + while (true) { if (JSON_HEDLEY_UNLIKELY(!unexpect_eof("string"))) @@ -18704,26 +18708,82 @@ class binary_reader } // a break marker closes the indefinite-length string; outside - // of one it falls through to the error below + // of one it falls through to the error below. Every chunk has + // been checked on its own, so there is nothing left to check. if (indefinite && current == 0xFF) { - return check_string_utf8(result, context); - } - - if (JSON_HEDLEY_UNLIKELY(!get_cbor_string_chunk(result, indefinite))) - { - return false; + return true; } if (!indefinite) { - return check_string_utf8(result, context); + return get_cbor_string_chunk(result, false) && check_string_utf8(result, context); + } + + if (error_handler == error_handler_t::keep) + { + // nothing to check: read the chunk directly into the result + if (JSON_HEDLEY_UNLIKELY(!get_cbor_string_chunk(result, true))) + { + return false; + } + } + else + { + // RFC 8949, Section 3.2.3: each chunk is a text string of its + // own and must be valid UTF-8 by itself, i.e., a code point + // cannot be spread over two chunks + chunk.clear(); + if (JSON_HEDLEY_UNLIKELY(!get_cbor_string_chunk(chunk, true) || !check_string_utf8(chunk, context))) + { + return false; + } + result.append(chunk.data(), chunk.size()); } get(); } } + /*! + @brief skips the tags in front of a CBOR object key + + A tag may wrap any data item, so it may also wrap a key. Like in front of a + value, the tags are skipped for the handlers ignore and store; with the + handler error they are left to @ref get_cbor_object_key, which rejects the + key. The tag loop is iterative, so the number of tags is not limited by the + recursion depth. + + @param[in] tag_handler how CBOR tags should be treated + + @return whether the tags could be skipped (false on EOF in a tag argument) + + @pre @a current is the first byte of the key + */ + bool skip_cbor_key_tags(const cbor_tag_handler_t tag_handler) + { + if (tag_handler == cbor_tag_handler_t::error) + { + return true; + } + + // 0xC0..0xDB: tag with the tag number in the head (0xC0..0xD7) or in + // the 1, 2, 4, or 8 bytes that follow (0xD8..0xDB) + while (current >= 0xC0 && current <= 0xDB) + { + if (current >= 0xD8) + { + std::uint64_t tag_to_ignore{}; + if (JSON_HEDLEY_UNLIKELY(!get_cbor_argument(tag_to_ignore))) + { + return false; + } + } + get(); + } + return true; + } + /*! @brief reads a CBOR object key @@ -19090,6 +19150,10 @@ class binary_reader if (top.is_object) { + if (JSON_HEDLEY_UNLIKELY(!skip_cbor_key_tags(tag_handler))) + { + return false; + } if (JSON_HEDLEY_UNLIKELY(!read_object_key<&binary_reader::get_cbor_object_key>(key))) { return false; @@ -21597,9 +21661,9 @@ class binary_reader // sequence), UBJSON, BJData, or BSON requires a decoder to reject // ill-formed UTF-8. Checking (and, with @ref error_handler_t::strict, // rejecting, or with `replace`/`ignore`, sanitizing) is opt-in via - // @ref error_handler, applied once the whole string (all chunks of - // an indefinite-length CBOR string included) has been assembled, by - // @ref check_string_utf8 at the call site. + // @ref error_handler, applied by @ref check_string_utf8 at the call + // site to the whole string or, for an indefinite-length CBOR string, + // to each chunk on its own (RFC 8949, Section 3.2.3). return get_bytes(len, "string", result); } diff --git a/tests/src/unit-cbor.cpp b/tests/src/unit-cbor.cpp index 9ee371495..44cfa7723 100644 --- a/tests/src/unit-cbor.cpp +++ b/tests/src/unit-cbor.cpp @@ -1807,6 +1807,93 @@ TEST_CASE("CBOR") CHECK(json::from_cbor(std::vector({0xa1, 0xff, 0x01}), true, false).is_discarded()); } + SECTION("tag in front of a key") + { + const auto ignore = json::cbor_tag_handler_t::ignore; + const auto store = json::cbor_tag_handler_t::store; + const json expected = {{"a", 10}}; + + // the tags are skipped in front of a text string key, like in + // front of a value + const std::vector> tagged = + { + {0xA1, 0xC0, 0x61, 0x61, 0x0A}, // tag in the head + {0xA1, 0xD8, 0x05, 0x61, 0x61, 0x0A}, // 1-byte tag number + {0xA1, 0xD9, 0x01, 0x2C, 0x61, 0x61, 0x0A}, // 2-byte tag number + {0xA1, 0xDA, 0x00, 0x01, 0x00, 0x00, 0x61, 0x61, 0x0A}, // 4-byte tag number + {0xA1, 0xDB, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x61, 0x61, 0x0A}, // 8-byte tag number + {0xA1, 0xC0, 0xD8, 0x05, 0xC1, 0x61, 0x61, 0x0A}, // chained tags + {0xBF, 0xC0, 0x61, 0x61, 0x0A, 0xFF}, // indefinite-length map + {0xBF, 0x61, 0x62, 0x01, 0xC0, 0x61, 0x61, 0x0A, 0xFF}, // second key tagged + {0xA1, 0xC0, 0x7F, 0x61, 0x61, 0xFF, 0x0A}, // indefinite-length text key + {0xA1, 0xC0, 0x61, 0x61, 0xC0, 0x0A}, // tagged key and value + }; + + for (const auto& input : tagged) + { + CAPTURE(input) + json const j_ignore = json::from_cbor(input, true, true, ignore); + json const j_store = json::from_cbor(input, true, true, store); + CHECK(j_ignore.at("a") == 10); + CHECK(j_store.at("a") == 10); + CHECK(j_ignore == j_store); + } + CHECK(json::from_cbor(tagged[0], true, true, ignore) == expected); + CHECK(json::from_cbor(tagged[1], true, true, store) == expected); + + // nested maps + CHECK(json::from_cbor(std::vector({0xA1, 0xC0, 0x61, 0x61, 0xA1, 0xD8, 0x05, 0x61, 0x62, 0x01}), true, true, ignore) == json{{"a", {{"b", 1}}}}); + + // the tag handler error still rejects every tag + for (const auto& input : tagged) + { + CAPTURE(input) + json _; + CHECK_THROWS_AS(_ = json::from_cbor(input), json::parse_error&); + } + { + json _; + CHECK_THROWS_WITH_AS(_ = json::from_cbor(std::vector({0xA1, 0xD8, 0x05, 0x61, 0x61, 0x0A})), "[json.exception.parse_error.113] parse error at byte 2: syntax error while parsing CBOR object key: only string keys are supported, but found a tag; last byte: 0xD8", json::parse_error&); + } + + // a tag does not turn a key of another type into a text string + { + json _; + CHECK_THROWS_WITH_AS(_ = json::from_cbor(std::vector({0xA1, 0xC0, 0x01, 0x01}), true, true, ignore), "[json.exception.parse_error.113] parse error at byte 3: syntax error while parsing CBOR object key: only string keys are supported, but found an unsigned integer; last byte: 0x01", json::parse_error&); + CHECK_THROWS_WITH_AS(_ = json::from_cbor(std::vector({0xA1, 0xD8, 0x05, 0x41, 0x61, 0x01}), true, true, store), "[json.exception.parse_error.113] parse error at byte 4: syntax error while parsing CBOR object key: only string keys are supported, but found a byte string; last byte: 0x41", json::parse_error&); + CHECK(json::from_cbor(std::vector({0xA1, 0xC0, 0x01, 0x01}), true, false, ignore).is_discarded()); + } + + // end of input after the tag, or inside the tag number + { + json _; + CHECK_THROWS_WITH_AS(_ = json::from_cbor(std::vector({0xA1, 0xC0}), true, true, ignore), "[json.exception.parse_error.110] parse error at byte 3: syntax error while parsing CBOR string: unexpected end of input", json::parse_error&); + CHECK_THROWS_AS(_ = json::from_cbor(std::vector({0xA1, 0xD8}), true, true, ignore), json::parse_error&); + CHECK_THROWS_AS(_ = json::from_cbor(std::vector({0xA1, 0xDB, 0x00, 0x00}), true, true, store), json::parse_error&); + CHECK_THROWS_AS(_ = json::from_cbor(std::vector({0xBF, 0xC0}), true, true, store), json::parse_error&); + } + + // SAX: the key event is reported without the tag (start_object, + // key, number_unsigned, end_object are four events) + { + const std::vector input = {0xA1, 0xC0, 0x61, 0x61, 0x0A}; + SaxCountdown four(4); + CHECK(json::sax_parse(input, &four, json::input_format_t::cbor, true, false, false, ignore)); + SaxCountdown three(3); + CHECK(!json::sax_parse(input, &three, json::input_format_t::cbor, true, false, false, ignore)); + SaxCountdown error_handler(4); + CHECK(!json::sax_parse(input, &error_handler, json::input_format_t::cbor)); + } + + // a long chain of tags needs no recursion + { + std::vector input = {0xA1}; + input.insert(input.end(), 100000, 0xC0); + input.insert(input.end(), {0x61, 0x61, 0x0A}); + CHECK(json::from_cbor(input, true, true, ignore) == expected); + } + } + SECTION("non-string key (see #2766 and #3381)") { // only text strings map to JSON object keys; any other key is @@ -1915,9 +2002,10 @@ TEST_CASE("CBOR") { json _; - // the chunks are concatenated as is, without checking that each - // chunk is valid UTF-8 on its own (RFC 8949, Section 3.2.3), so - // a code point split across two chunks yields a valid string + // with the default error handler keep, the chunks are concatenated + // as is, without checking that each chunk is valid UTF-8 on its + // own (RFC 8949, Section 3.2.3), so a code point split across two + // chunks yields a valid string CHECK_NOTHROW(_ = json::from_cbor(std::vector({0x7f, 0x61, 0xc3, 0x61, 0xa9, 0xff}))); CHECK(_ == "\xc3\xa9"); CHECK(_.dump() == "\"\xc3\xa9\""); @@ -1937,6 +2025,63 @@ TEST_CASE("CBOR") CHECK(json::from_cbor(std::vector({0x7f, 0x62, 0xc3, 0xa9, 0x62, 0xc3, 0xb6, 0xff})) == "\xc3\xa9\xc3\xb6"); } + SECTION("ill-formed UTF-8 in a chunk of an indefinite-length string (RFC 8949, Section 3.2.3)") + { + const auto strict = json::error_handler_t::strict; + const auto replace = json::error_handler_t::replace; + const auto ignore = json::error_handler_t::ignore; + const auto keep = json::error_handler_t::keep; + const auto tags = json::cbor_tag_handler_t::error; + + // a code point split over two chunks + const std::vector split = {0x7f, 0x61, 0xc3, 0x61, 0xa9, 0xff}; + // a 4-byte code point split over two chunks + const std::vector split_emoji = {0x7f, 0x62, 0xf0, 0x9f, 0x62, 0x98, 0x80, 0xff}; + // the same in an object key + const std::vector split_key = {0xa1, 0x7f, 0x61, 0xc3, 0x61, 0xa9, 0xff, 0x01}; + // a valid chunk followed by one with a truncated code point + const std::vector truncated = {0x7f, 0x62, 0x41, 0xc3, 0x61, 0xa9, 0xff}; + + // strict: the string is invalid + for (const auto& input : + { + split, split_emoji, split_key, truncated + }) + { + CAPTURE(input) + json _; + CHECK_THROWS_AS(_ = json::from_cbor(input, true, true, tags, strict), json::parse_error&); + CHECK(json::from_cbor(input, true, false, tags, strict).is_discarded()); + } + { + json _; + CHECK_THROWS_WITH_AS(_ = json::from_cbor(split, true, true, tags, strict), "[json.exception.parse_error.113] parse error at byte 3: syntax error while parsing CBOR string: invalid string: ill-formed UTF-8 byte", json::parse_error&); + CHECK_THROWS_WITH_AS(_ = json::from_cbor(split_key, true, true, tags, strict), "[json.exception.parse_error.113] parse error at byte 4: syntax error while parsing CBOR key: invalid string: ill-formed UTF-8 byte", json::parse_error&); + } + + // replace and ignore: every chunk is sanitized on its own + CHECK(json::from_cbor(split, true, true, tags, replace) == "\xEF\xBF\xBD\xEF\xBF\xBD"); + CHECK(json::from_cbor(split, true, true, tags, ignore) == ""); + CHECK(json::from_cbor(truncated, true, true, tags, replace) == "A\xEF\xBF\xBD\xEF\xBF\xBD"); + CHECK(json::from_cbor(truncated, true, true, tags, ignore) == "A"); + CHECK(json::from_cbor(split_key, true, true, tags, replace) == json{{"\xEF\xBF\xBD\xEF\xBF\xBD", 1}}); + + // keep: unchanged, the chunks are concatenated as is + CHECK(json::from_cbor(split, true, true, tags, keep) == "\xc3\xa9"); + CHECK(json::from_cbor(split_emoji, true, true, tags, keep) == "\xf0\x9f\x98\x80"); + + // conforming input is unaffected by the error handler + const std::vector valid = {0x7f, 0x62, 0xc3, 0xa9, 0x64, 0xf0, 0x9f, 0x98, 0x80, 0x60, 0xff}; + for (const auto handler : + { + strict, replace, ignore, keep + }) + { + CHECK(json::from_cbor(valid, true, true, tags, handler) == "\xc3\xa9\xf0\x9f\x98\x80"); + CHECK(json::from_cbor(std::vector({0x7f, 0xff}), true, true, tags, handler) == ""); + } + } + SECTION("many chunks in indefinite-length string") { constexpr std::size_t chunks = 100000; From 2bbe80c0435dba2805747d81482fd2acc7861868 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Mon, 26 Oct 2026 18:25:44 +0100 Subject: [PATCH 08/18] Document MessagePack ext types and fix related doc errors (#5928) * Document MessagePack ext types and fix related doc errors Add an "Extension types (ext, fixext)" section to the MessagePack page: ext/fixext are read as binary, the signed ext type is exposed as an unsigned subtype 0..255 (-1 reads as 255), Timestamp is not interpreted, subtypes are always written as ext/fixext, and subtypes above 255 throw out_of_range.415. Add the 0xC1 row to the deserialization table, note that duplicate keys keep the last value, and correct the NaN/Infinity explanation, the "31 bytes / 15 elements" wording, the ill-formed UTF-8 sentence, and the generic 0xC1-key error message. Add fixext16 and the unsigned subtype mapping to binary_t.md and binary_values.md, reword "maximum of the MessagePack ext type" in to_msgpack.md and exceptions.md, and mark MessagePack deserialization as incomplete (non-string keys) in the binary formats overview. Signed-off-by: Niels Lohmann * Address review of the MessagePack documentation Name parse_error.113 for a 0xC1 map key, move the duplicate-key sentence out of the ext section, and keep the 0xC1 row in marker order. Signed-off-by: Niels Lohmann --------- Signed-off-by: Niels Lohmann --- docs/mkdocs/docs/api/basic_json/binary_t.md | 5 ++- docs/mkdocs/docs/api/basic_json/to_msgpack.md | 2 +- .../docs/features/binary_formats/index.md | 2 +- .../features/binary_formats/messagepack.md | 37 ++++++++++++++++--- docs/mkdocs/docs/features/binary_values.md | 5 ++- docs/mkdocs/docs/home/exceptions.md | 2 +- 6 files changed, 41 insertions(+), 12 deletions(-) diff --git a/docs/mkdocs/docs/api/basic_json/binary_t.md b/docs/mkdocs/docs/api/basic_json/binary_t.md index da13c22a2..2e9517429 100644 --- a/docs/mkdocs/docs/api/basic_json/binary_t.md +++ b/docs/mkdocs/docs/api/basic_json/binary_t.md @@ -90,8 +90,9 @@ type `#!cpp binary_t*` must be dereferenced. - MessagePack - If a subtype is given and the binary array contains exactly 1, 2, 4, 8, or 16 elements, the fixext family (fixext1, - fixext2, fixext4, fixext8) is used. For other sizes, the ext family (ext8, ext16, ext32) is used. The subtype is - then added as a signed 8-bit integer. + fixext2, fixext4, fixext8, fixext16) is used. For other sizes, the ext family (ext8, ext16, ext32) is used. The + subtype is then written as the signed 8-bit ext type: subtypes 128 to 255 map to the negative (reserved) ext + types, and on reading the ext type is exposed as an unsigned subtype from 0 to 255. - If no subtype is given, the bin family (bin8, bin16, bin32) is used. - BSON diff --git a/docs/mkdocs/docs/api/basic_json/to_msgpack.md b/docs/mkdocs/docs/api/basic_json/to_msgpack.md index dca66271d..181948ef1 100644 --- a/docs/mkdocs/docs/api/basic_json/to_msgpack.md +++ b/docs/mkdocs/docs/api/basic_json/to_msgpack.md @@ -53,7 +53,7 @@ Strong guarantee: if an exception is thrown, there are no changes in the JSON va value, array, or object exceeds 4294967295, the maximum MessagePack can store; example: `"MessagePack length 4294967296 exceeds maximum of 4294967295"` - Throws [`out_of_range.415`](../../home/exceptions.md#jsonexceptionout_of_range415) if the subtype of a binary value - exceeds 255, the maximum of the MessagePack ext type; example: + exceeds 255, the largest value the library can store in the one-byte MessagePack ext type; example: `"subtype 70000 is too large for the MessagePack ext type (max 255)"` - Throws [type_error.316](../../home/exceptions.md#jsonexceptiontype_error316) if a string or object key in `j` is not valid UTF-8 and `error_handler` is `strict` diff --git a/docs/mkdocs/docs/features/binary_formats/index.md b/docs/mkdocs/docs/features/binary_formats/index.md index 8024b99cf..9c55a9a52 100644 --- a/docs/mkdocs/docs/features/binary_formats/index.md +++ b/docs/mkdocs/docs/features/binary_formats/index.md @@ -22,7 +22,7 @@ to efficiently encode JSON values to byte vectors and to decode such vectors. | BON8 | incomplete: no unsigned integers above int64 | complete | | BSON | incomplete: top-level value must be an object | incomplete, but all JSON types are supported | | CBOR | complete | incomplete, but all JSON types are supported | -| MessagePack | complete | complete | +| MessagePack | complete | incomplete, but all JSON types are supported | | UBJSON | complete | complete | ### Binary values diff --git a/docs/mkdocs/docs/features/binary_formats/messagepack.md b/docs/mkdocs/docs/features/binary_formats/messagepack.md index 90e4c295e..cca6e7660 100644 --- a/docs/mkdocs/docs/features/binary_formats/messagepack.md +++ b/docs/mkdocs/docs/features/binary_formats/messagepack.md @@ -11,7 +11,7 @@ extra byte in addition to the strings themselves. ## Byte layout -MessagePack encodes each value as a one-byte *format marker*, optionally followed by a length or the payload. The marker range is partitioned to keep common values small: small non-negative integers (0 to 127) and small negative integers (-32 to -1) are the marker byte itself; short strings, arrays, and maps (up to 31 or 15 elements) hide their length in the marker's lower bits. Larger values use dedicated markers that announce the width (8, 16, 32, or 64 bits) of the length or number that follows, in big-endian byte order. Like CBOR, containers are length-prefixed, so no terminator is needed. +MessagePack encodes each value as a one-byte *format marker*, optionally followed by a length or the payload. The marker range is partitioned to keep common values small: small non-negative integers (0 to 127) and small negative integers (-32 to -1) are the marker byte itself; short strings, arrays, and maps (up to 31 bytes for strings and up to 15 elements for arrays and maps) hide their length in the marker's lower bits. Larger values use dedicated markers that announce the width (8, 16, 32, or 64 bits) of the length or number that follows, in big-endian byte order. Like CBOR, containers are length-prefixed, so no terminator is needed. Consider the following JSON value (shown formatted for readability): @@ -109,8 +109,9 @@ specification: !!! note Prior to version 3.13.0, NaN and Infinity were instead serialized as a MessagePack float 64 (type 0xCB, 9 bytes - total), because the check used to select the smaller float 32 encoding compared magnitudes with NaN, which is - always `false` and caused the float 32 path to be skipped. + total), because the check used to select the smaller float 32 encoding failed for NaN (all comparisons are `false`) + and for Infinity and -Infinity (outside the float range checked against `std::numeric_limits::lowest()` and + `max()`), so the float 32 path was skipped. ??? example "Example: serialize a JSON value to MessagePack" @@ -135,6 +136,7 @@ The library maps MessagePack types to JSON value types as follows: | fixarray | array | 0x90..0x9F | | fixstr | string | 0xA0..0xBF | | nil | `null` | 0xC0 | +| (never used) | – | 0xC1 | | false | `false` | 0xC2 | | true | `true` | 0xC3 | | float 32 | number_float | 0xCA | @@ -167,6 +169,30 @@ The library maps MessagePack types to JSON value types as follows: | fixext 16 | binary | 0xD8 | | negative fixint | number_integer | 0xE0-0xFF | +The byte 0xC1 is reserved as "never used" by the specification. As a value it is rejected with +[`parse_error.112`](../../home/exceptions.md#jsonexceptionparse_error112) (`invalid byte: 0xC1`); as a map key it is +rejected with [`parse_error.113`](../../home/exceptions.md#jsonexceptionparse_error113) and the generic "expected length +specification" message, because the key is read as a string. + +Duplicate keys in a map are accepted; the last value wins. + +### Extension types (ext, fixext) + +MessagePack ext and fixext values are read as binary values, and the ext type byte is stored as the binary subtype. +Although the specification defines the type as a signed 8-bit integer (0 to 127 for applications, -128 to -1 reserved, +with -1 being the Timestamp extension), the library reads it as an unsigned byte: a negative ext type `n` becomes +subtype `256+n`, so ext type -1 is read as subtype 255 and -128 as subtype 128. The Timestamp extension is not +interpreted; it is returned as binary with subtype 255. Use +[`has_subtype()`](../../api/byte_container_with_subtype/has_subtype.md) to tell a `bin` value (no subtype) from an ext +value (subtype, even if it is 0). + +When serializing, a binary value with a subtype is always written as fixext (1, 2, 4, 8, or 16 bytes) or ext, even +for subtype 0 (`d6 00 01 02 03 04`); a binary value without a subtype is written as bin. Subtypes 128 to 255 are +written as the reserved negative ext types (255 as -1), so `json::binary({1,2,3,4}, 255)` becomes +`d6 ff 01 02 03 04`, which other MessagePack libraries decode as a 32-bit timestamp. A subtype above 255 throws +[`out_of_range.415`](../../home/exceptions.md#jsonexceptionout_of_range415). The payload length is limited to +4294967295 bytes like all other lengths. + !!! info Any MessagePack output created by `to_msgpack` can be successfully parsed by `from_msgpack`. @@ -177,7 +203,8 @@ The library maps MessagePack types to JSON value types as follows: JSON-compatible [profile](https://github.com/msgpack/msgpack/blob/master/spec.md#profile) sketched in the MessagePack specification, this library restricts map keys to `str` values. Maps with keys of any other type are rejected with a [`parse_error.113`](../../home/exceptions.md#jsonexceptionparse_error113) exception (or, with - `allow_exceptions` set to `false`, a discarded value) naming the type of the key that was found, for instance: + `allow_exceptions` set to `false`, a discarded value). For most key types the message names the type that was + found, for instance: ``` [json.exception.parse_error.113] parse error at byte 2: syntax error while parsing MessagePack object key: only string keys are supported, but found nil; last byte: 0xC0 @@ -189,7 +216,7 @@ The library maps MessagePack types to JSON value types as follows: !!! warning "Ill-formed UTF-8 in string values" The MessagePack specification explicitly allows a `str` value (`fixstr`, `str 8`, `str 16`, `str 32`) to contain - a byte sequence that is not valid UTF-8, and expects a deserializer to hand the original bytes back unchanged. + a byte sequence that is not valid UTF-8, and recommends that a deserializer provide a way to obtain the original bytes. This library follows that by default: with its [`error_handler`](../../api/basic_json/from_msgpack.md) parameter left at `keep` (the default), `from_msgpack()` reads `str` bytes (object keys included) as-is, without validating them, so such a value diff --git a/docs/mkdocs/docs/features/binary_values.md b/docs/mkdocs/docs/features/binary_values.md index dd683f469..f5a87472b 100644 --- a/docs/mkdocs/docs/features/binary_values.md +++ b/docs/mkdocs/docs/features/binary_values.md @@ -324,8 +324,9 @@ representation using the length of the byte array. ### MessagePack [MessagePack](binary_formats/messagepack.md) supports binary values and subtypes. If a subtype is given, the ext family -is used. The library will choose the smallest representation among fixext1, fixext2, fixext4, fixext8, ext8, ext16, and -ext32. The subtype is then added as a signed 8-bit integer. +is used. The library will choose the smallest representation among fixext1, fixext2, fixext4, fixext8, fixext16, ext8, +ext16, and ext32. The subtype is then written as the signed 8-bit ext type: subtypes 128 to 255 map to the negative +(reserved) ext types, and on reading the ext type is exposed as an unsigned subtype from 0 to 255. If no subtype is given, the bin family (bin8, bin16, bin32) is used. diff --git a/docs/mkdocs/docs/home/exceptions.md b/docs/mkdocs/docs/home/exceptions.md index 1b8436f91..aedfe504e 100644 --- a/docs/mkdocs/docs/home/exceptions.md +++ b/docs/mkdocs/docs/home/exceptions.md @@ -1160,7 +1160,7 @@ A JSON Patch `move` operation's `"from"` location is a proper prefix of its `"pa ### json.exception.out_of_range.415 MessagePack's ext type and BSON's binary subtype are each stored in a single byte. This exception is thrown when serializing a -[`byte_container_with_subtype`](../api/byte_container_with_subtype/index.md) whose subtype exceeds 255. +[`byte_container_with_subtype`](../api/byte_container_with_subtype/index.md) whose subtype exceeds 255, the largest value the library can store in that byte. !!! failure "Example message" From 0c3570d44a95f096b2c5131c0b952899084261c9 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Mon, 26 Oct 2026 18:25:45 +0100 Subject: [PATCH 09/18] Validate the UBJSON/BJData container type marker for count 0; fix UBJSON docs (#5929) * Validate the optimized-container type marker for count 0 in UBJSON and BJData The '$' type marker of an optimized container was only checked when an element was read, so containers with count 0 accepted any marker: `[$x#i0`, `{$x#i0` and `[$E#i0` parsed as empty containers although a non-empty container with the same marker is rejected and the UBJSON specification requires a legal marker after '$'. get_ubjson_size_type() now checks the marker right after '$' against the allowed set (UBJSON: Z N T F i U I l L d D H C S [ {; BJData: B U i u I m l M L h d D C, with the existing exclusion of T F Z N S H [ {) and reports parse_error.112 "marker ... is not a permitted optimized array type". Input that was accepted and is now rejected: an optimized container with count 0 and an invalid '$' type marker. Valid input is unaffected. Two existing tests that used an invalid marker incidentally ("[$#" and a negative-count case) were adapted. Signed-off-by: Niels Lohmann * Correct and complete the UBJSON documentation Fix statements that disagree with the behavior (checked with probes): - `Z` is null; the unused marker is `N`. - `$` requires `#`; use_type with use_size=false throws other_error.502 only for non-empty arrays, objects and binary values. - NaN and infinity are written as IEEE 754 float64 and read back unchanged; the UBJSON specification encodes infinity as null. - `H` is documented (semantics, parse_error.115, out_of_range.406); the "complete mapping" claim is qualified. - use_type "same type" means same marker. - Long Z/T/F arrays lose only the `$` annotation; the `#` count and the element markers are kept. - from_ubjson lists the 112, 113, 115, 406 and 408 causes that occur for UBJSON, without the BJData-only ND-array wording. - Duplicate object keys keep the last value; the `$` type marker is validated even for count 0. - Smaller fixes: float32 row, "map" -> "object", https link, exception 110 strict case, to_ubjson history of discarded values. Signed-off-by: Niels Lohmann * Regenerate single_include and apply astyle Result of make amalgamate (including make pretty) for the preceding changes. Signed-off-by: Niels Lohmann * Address review of the UBJSON documentation Clarify that empty containers carry no $ annotation of their own and note that a counted N object is not supported. Signed-off-by: Niels Lohmann * docs: format tables, use the Oxford comma, drop discarded-value remarks Reformat the two UBJSON mapping tables that this PR changed (uniform column widths, padded cells), use the serial comma in lists of three or more in the added text, and restore the original type_error.321 bullet in to_ubjson.md. Signed-off-by: Niels Lohmann * Fix narrowing of the optimized-container count in BJData and UBJSON tests Signed-off-by: Niels Lohmann --------- Signed-off-by: Niels Lohmann --- .../mkdocs/docs/api/basic_json/from_ubjson.md | 15 +- .../docs/features/binary_formats/ubjson.md | 133 +++++++++++------- docs/mkdocs/docs/home/exceptions.md | 9 +- .../nlohmann/detail/input/binary_reader.hpp | 46 ++++++ single_include/nlohmann/json.hpp | 46 ++++++ tests/src/unit-bjdata.cpp | 61 +++++++- tests/src/unit-ubjson.cpp | 54 ++++++- 7 files changed, 304 insertions(+), 60 deletions(-) diff --git a/docs/mkdocs/docs/api/basic_json/from_ubjson.md b/docs/mkdocs/docs/api/basic_json/from_ubjson.md index 3881f9ea4..ca947ef05 100644 --- a/docs/mkdocs/docs/api/basic_json/from_ubjson.md +++ b/docs/mkdocs/docs/api/basic_json/from_ubjson.md @@ -79,11 +79,18 @@ Strong guarantee: if an exception is thrown, there are no changes in the JSON va - Throws [parse_error.110](../../home/exceptions.md#jsonexceptionparse_error110) if the given input ends prematurely or the end of the file was not reached when `strict` was set to true -- Throws [parse_error.112](../../home/exceptions.md#jsonexceptionparse_error112) if a parse error occurs +- Throws [parse_error.112](../../home/exceptions.md#jsonexceptionparse_error112) if an invalid type marker is read + (including a type marker after `$` of an optimized container, even if its count is 0), or if `$` is not followed by `#` - Throws [parse_error.113](../../home/exceptions.md#jsonexceptionparse_error113) if a string could not be parsed - successfully, or if a string value or object key is not valid UTF-8 and `error_handler` is `strict` -- Throws [out_of_range.408](../../home/exceptions.md#jsonexceptionout_of_range408) if the size of an optimized container - or n-dimensional array cannot be represented by `std::size_t` + successfully (negative or non-integer length, `C` followed by a byte above 0x7F), if the count of an optimized + container is negative, or if a string value or object key is not valid UTF-8 and `error_handler` is `strict` +- Throws [parse_error.115](../../home/exceptions.md#jsonexceptionparse_error115) if the text of a high-precision number + (`H`) is not a valid JSON number +- Throws [out_of_range.406](../../home/exceptions.md#jsonexceptionout_of_range406) if a high-precision number, or a + float64 when `number_float_t` is `float`, does not fit into `number_float_t` +- Throws [out_of_range.408](../../home/exceptions.md#jsonexceptionout_of_range408) if the count of an optimized + container exceeds the capacity of the container (or of `std::size_t`), or if an optimized array of `Z`, `T`, or `F` has + more than 1,048,576 elements ## Complexity diff --git a/docs/mkdocs/docs/features/binary_formats/ubjson.md b/docs/mkdocs/docs/features/binary_formats/ubjson.md index ba12ee4f6..0e7ea2ca1 100644 --- a/docs/mkdocs/docs/features/binary_formats/ubjson.md +++ b/docs/mkdocs/docs/features/binary_formats/ubjson.md @@ -5,11 +5,11 @@ achieve the generality of JSON, combined with being much easier to process than !!! abstract "References" - - [UBJSON Website](http://ubjson.org) + - [UBJSON Website](https://ubjson.org) ## Byte layout -UBJSON is built from just a one-byte, human-readable ASCII *type marker* (for example, `i` for int8, `S` for string, `[` for array) followed by the payload of that type. Strings and the like are prefixed with their length, which is itself a UBJSON integer value including its marker. Numbers are stored in big-endian byte order. Containers can be written in two ways: as an *unoptimized* container that is opened with `[` or `{` and closed with `]` or `}`, or as an *optimized* container that declares an element type (`$`) and/or a count (`#`) after the opening marker, which saves per-element markers and (for a count) the closing marker. +UBJSON is built from just a one-byte, human-readable ASCII *type marker* (for example, `i` for int8, `S` for string, `[` for array) followed by the payload of that type. Strings and the like are prefixed with their length, which is itself a UBJSON integer value including its marker. Numbers are stored in big-endian byte order. Containers can be written in two ways: as an *unoptimized* container that is opened with `[` or `{` and closed with `]` or `}`, or as an *optimized* container that declares a count (`#`) after the opening marker, optionally preceded by an element type (`$`); an element type is only allowed together with a count. An optimized container saves the closing marker and, with a type, the per-element markers. Consider the following JSON value (shown formatted for readability): @@ -44,29 +44,29 @@ Consider the following JSON value (shown formatted for readability): The library uses the following mapping from JSON values types to UBJSON types according to the UBJSON specification: -| JSON value type | value/range | UBJSON type | marker | -|-----------------|-------------------------------------------|----------------|--------| -| null | `null` | null | `Z` | -| boolean | `true` | true | `T` | -| boolean | `false` | false | `F` | -| number_integer | -9223372036854775808..-2147483649 | int64 | `L` | -| number_integer | -2147483648..-32769 | int32 | `l` | -| number_integer | -32768..-129 | int16 | `I` | -| number_integer | -128..127 | int8 | `i` | -| number_integer | 128..255 | uint8 | `U` | -| number_integer | 256..32767 | int16 | `I` | -| number_integer | 32768..2147483647 | int32 | `l` | -| number_integer | 2147483648..9223372036854775807 | int64 | `L` | -| number_unsigned | 0..127 | int8 | `i` | -| number_unsigned | 128..255 | uint8 | `U` | -| number_unsigned | 256..32767 | int16 | `I` | -| number_unsigned | 32768..2147483647 | int32 | `l` | -| number_unsigned | 2147483648..9223372036854775807 | int64 | `L` | -| number_unsigned | 9223372036854775808..18446744073709551615 | high-precision | `H` | -| number_float | *any value* | float64 | `D` | -| string | *with shortest length indicator* | string | `S` | -| array | *see notes on optimized format* | array | `[` | -| object | *see notes on optimized format* | map | `{` | +| JSON value type | value/range | UBJSON type | marker | +|-----------------|-------------------------------------------|--------------------------------------------------|-----------| +| null | `null` | null | `Z` | +| boolean | `true` | true | `T` | +| boolean | `false` | false | `F` | +| number_integer | -9223372036854775808..-2147483649 | int64 | `L` | +| number_integer | -2147483648..-32769 | int32 | `l` | +| number_integer | -32768..-129 | int16 | `I` | +| number_integer | -128..127 | int8 | `i` | +| number_integer | 128..255 | uint8 | `U` | +| number_integer | 256..32767 | int16 | `I` | +| number_integer | 32768..2147483647 | int32 | `l` | +| number_integer | 2147483648..9223372036854775807 | int64 | `L` | +| number_unsigned | 0..127 | int8 | `i` | +| number_unsigned | 128..255 | uint8 | `U` | +| number_unsigned | 256..32767 | int16 | `I` | +| number_unsigned | 32768..2147483647 | int32 | `l` | +| number_unsigned | 2147483648..9223372036854775807 | int64 | `L` | +| number_unsigned | 9223372036854775808..18446744073709551615 | high-precision | `H` | +| number_float | *any value* | float64 (float32 if `number_float_t` is `float`) | `D` (`d`) | +| string | *with shortest length indicator* | string | `S` | +| array | *see notes on optimized format* | array | `[` | +| object | *see notes on optimized format* | object | `{` | !!! success "Complete mapping" @@ -93,20 +93,27 @@ The library uses the following mapping from JSON values types to UBJSON types ac The following markers are not used in the conversion: - - `Z`: no-op values are not created. + - `N`: no-op values are never written (they are skipped when reading). - `C`: single-byte strings are serialized with `S` markers. !!! info "NaN/infinity handling" - If NaN or Infinity are stored inside a JSON number, they are serialized properly. This behavior differs from the - [`dump()`](../../api/basic_json/dump.md) function which serializes NaN or Infinity to `null`. + NaN and infinity are written as IEEE 754 float64 values (`D`) and read back unchanged. This differs from + [`dump()`](../../api/basic_json/dump.md), which serializes them to `null`, and from the UBJSON specification, which + encodes infinity as `null` (it does not mention NaN); other UBJSON readers may therefore not accept these values. !!! info "Optimized formats" The optimized formats for containers are supported: Parameter `use_size` adds size information to the beginning of a container and removes the closing marker. Parameter `use_type` further checks whether all elements of a container have the same type and adds the type marker to the beginning of the container. The `use_type` parameter must only be - used together with `use_size = true`. + used together with `use_size = true`: combining `use_type` with `use_size = false` throws + [`other_error.502`](../../home/exceptions.md#jsonexceptionother_error502) if the value contains a non-empty array, + object, or binary value. + + Two elements have the same type if they are written with the same type marker. For example, `[1, 2]` is written with + the type `i`, but `[1, 200]` (`i`, `U`), `[true, false]` (`T`, `F`), and `[1, 2.5]` (`i`, `D`) are not; an empty + container is never given a `$` annotation of its own (an array of empty arrays is still typed `[`). Note that `use_size = true` alone may result in larger representations - the benefit of this parameter is that the receiving side is immediately informed on the number of elements of the container. @@ -115,8 +122,9 @@ The library uses the following mapping from JSON values types to UBJSON types ac already is the value. Its declared count is therefore the only thing that decides how much memory the receiving side allocates, and a handful of bytes can describe billions of elements. `from_ubjson` rejects such an array with [`out_of_range.408`](../../home/exceptions.md#jsonexceptionout_of_range408) when the count exceeds 1,048,576 - (`1 << 20`), and `to_ubjson` writes longer arrays of these types without the annotation, so any value it produces - can be read back. + (`1 << 20`), and `to_ubjson` writes longer arrays of these types in the counted but untyped form: the `$` type marker is + dropped, the `#` count is kept, and every element is written with its own marker (`[#l 1048577 Z Z ...`), so any value + it produces can be read back. Objects and untyped counted arrays are not subject to this limit. !!! info "Binary values" @@ -140,27 +148,54 @@ The library uses the following mapping from JSON values types to UBJSON types ac The library maps UBJSON types to JSON value types as follows: -| UBJSON type | JSON value type | marker | -|-------------|-----------------------------------------|--------| -| no-op | *no value, next value is read* | `N` | -| null | `null` | `Z` | -| false | `false` | `F` | -| true | `true` | `T` | -| float32 | number_float | `d` | -| float64 | number_float | `D` | -| uint8 | number_unsigned | `U` | -| int8 | number_integer | `i` | -| int16 | number_integer | `I` | -| int32 | number_integer | `l` | -| int64 | number_integer | `L` | -| string | string | `S` | -| char | string | `C` | -| array | array (optimized values are supported) | `[` | -| object | object (optimized values are supported) | `{` | +| UBJSON type | JSON value type | marker | +|----------------|--------------------------------------------------|--------| +| no-op | *no value, next value is read* | `N` | +| null | `null` | `Z` | +| false | `false` | `F` | +| true | `true` | `T` | +| float32 | number_float | `d` | +| float64 | number_float | `D` | +| uint8 | number_unsigned | `U` | +| int8 | number_integer | `i` | +| int16 | number_integer | `I` | +| int32 | number_integer | `l` | +| int64 | number_integer | `L` | +| string | string | `S` | +| char | string | `C` | +| high-precision | number_unsigned, number_integer, or number_float | `H` | +| array | array (optimized values are supported) | `[` | +| object | object (optimized values are supported) | `{` | + +!!! info "High-precision numbers" + + The payload of `H` is a length-prefixed string that is parsed as a JSON number (surrounding whitespace is ignored). + Integer text in the range of `number_unsigned_t` (non-negative) or `number_integer_t` (negative) is stored as such; + any other valid number text (larger or smaller integers, fractions, and exponents such as `1e2`) is stored as + `number_float`, so precision may be lost. Text that is not a valid JSON number raises + [`parse_error.115`](../../home/exceptions.md#jsonexceptionparse_error115), a number outside the range of + `number_float_t` (`1e999`) raises [`out_of_range.406`](../../home/exceptions.md#jsonexceptionout_of_range406). !!! success "Complete mapping" - The mapping is **complete** in the sense that any UBJSON value can be converted to a JSON value. + Every well-formed UBJSON value can be converted to a JSON value, with these exceptions: a high-precision number + outside the range of `number_float_t` ([`out_of_range.406`](../../home/exceptions.md#jsonexceptionout_of_range406)), + an optimized array of `Z`, `T`, or `F` with more than 1,048,576 elements, and a count that exceeds the capacity of the + container ([`out_of_range.408`](../../home/exceptions.md#jsonexceptionout_of_range408)). High-precision numbers that + do not fit an integer type are stored as floating-point numbers and may lose precision. Ill-formed input is rejected + with a parse error. + +!!! info "Duplicate object keys" + + If an object contains the same key more than once, the last value is kept (the UBJSON specification does not + address duplicate keys). + +!!! info "Type marker of optimized containers" + + The type marker after `$` must be one of `Z`, `N`, `T`, `F`, `i`, `U`, `I`, `l`, `L`, `d`, `D`, `H`, `C`, `S`, `[`, or + `{`. It is checked even if the count is 0; any other marker raises + [`parse_error.112`](../../home/exceptions.md#jsonexceptionparse_error112). A counted object typed `N` (`{$N#i1 ...`) + is not supported. !!! warning "Ill-formed UTF-8 in string values and object keys" diff --git a/docs/mkdocs/docs/home/exceptions.md b/docs/mkdocs/docs/home/exceptions.md index aedfe504e..ddf9d69cd 100644 --- a/docs/mkdocs/docs/home/exceptions.md +++ b/docs/mkdocs/docs/home/exceptions.md @@ -330,7 +330,7 @@ where they cannot be resolved. ### json.exception.parse_error.110 When parsing a [binary format](../features/binary_formats/index.md), the byte vector ends before the complete value has -been read. +been read, or (with `strict = true`) further bytes follow the value. !!! failure "Example messages" @@ -1045,8 +1045,8 @@ or the encoded length for [CBOR](../features/binary_formats/cbor.md). The exception is also thrown for a [UBJSON](../features/binary_formats/ubjson.md) array of a type that is encoded by its marker alone (`Z`, `T` or `F`) whose declared count exceeds 1,048,576 (`1 << 20`). Such an array has no payload, so its count alone decides how much memory is allocated, and a handful of bytes would otherwise describe billions of values. -[`to_ubjson`](../api/basic_json/to_ubjson.md) writes longer arrays of these types without the size and type annotation, -so any value it produces can still be read back. +[`to_ubjson`](../api/basic_json/to_ubjson.md) writes longer arrays of these types without the `$` type annotation (the `#` +count and one marker per element are kept), so any value it produces can still be read back. !!! failure "Example messages" @@ -1236,7 +1236,8 @@ A JSON Patch operation 'test' failed. The unsuccessful operation is also printed ### json.exception.other_error.502 [`to_ubjson`](../api/basic_json/to_ubjson.md) and [`to_bjdata`](../api/basic_json/to_bjdata.md) were called with -`use_type = true` but `use_size = false`. UBJSON requires a size marker (`#`) after a type marker (`$`). +`use_type = true` but `use_size = false` for a value that contains a non-empty array, object, or binary value. UBJSON +requires a size marker (`#`) after a type marker (`$`); scalars and empty containers do not throw. !!! failure "Example message" diff --git a/include/nlohmann/detail/input/binary_reader.hpp b/include/nlohmann/detail/input/binary_reader.hpp index 929ac7573..4c8d6ed0f 100644 --- a/include/nlohmann/detail/input/binary_reader.hpp +++ b/include/nlohmann/detail/input/binary_reader.hpp @@ -3000,6 +3000,14 @@ class binary_reader return false; } + // validate the type marker up front: otherwise it would only be + // checked when an element is read, and a container with count 0 + // would accept any marker + if (JSON_HEDLEY_UNLIKELY(!is_valid_optimized_type(result.second))) + { + return optimized_type_error(); + } + get_ignore_noop(); if (JSON_HEDLEY_UNLIKELY(current != '#')) { @@ -4496,6 +4504,44 @@ class binary_reader std::size_t bon8_pushback_size = 0; JSON_PRIVATE_UNLESS_TESTED: + /*! + @brief check whether @a marker is a type marker that may follow '$' + UBJSON: Z N T F i U I l L d D H C S [ {; BJData: the numeric markers and C + (BJData's excluded markers are rejected separately). + */ + bool is_valid_optimized_type(const char_int_type marker) const noexcept + { + switch (marker) + { + case 'i': + case 'U': + case 'I': + case 'l': + case 'L': + case 'd': + case 'D': + case 'C': + return true; + case 'Z': + case 'N': + case 'T': + case 'F': + case 'H': + case 'S': + case '[': + case '{': + return input_format != input_format_t::bjdata; + case 'B': + case 'u': + case 'm': + case 'M': + case 'h': + return input_format == input_format_t::bjdata; + default: + return false; + } + } + /*! @brief whether @a marker is excluded from BJData's optimized ND-array types @return whether @a marker is one of 'F', 'H', 'N', 'S', 'T', 'Z', '[', '{' diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 5db064c63..c4b7f159a 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -20467,6 +20467,14 @@ class binary_reader return false; } + // validate the type marker up front: otherwise it would only be + // checked when an element is read, and a container with count 0 + // would accept any marker + if (JSON_HEDLEY_UNLIKELY(!is_valid_optimized_type(result.second))) + { + return optimized_type_error(); + } + get_ignore_noop(); if (JSON_HEDLEY_UNLIKELY(current != '#')) { @@ -21963,6 +21971,44 @@ class binary_reader std::size_t bon8_pushback_size = 0; JSON_PRIVATE_UNLESS_TESTED: + /*! + @brief check whether @a marker is a type marker that may follow '$' + UBJSON: Z N T F i U I l L d D H C S [ {; BJData: the numeric markers and C + (BJData's excluded markers are rejected separately). + */ + bool is_valid_optimized_type(const char_int_type marker) const noexcept + { + switch (marker) + { + case 'i': + case 'U': + case 'I': + case 'l': + case 'L': + case 'd': + case 'D': + case 'C': + return true; + case 'Z': + case 'N': + case 'T': + case 'F': + case 'H': + case 'S': + case '[': + case '{': + return input_format != input_format_t::bjdata; + case 'B': + case 'u': + case 'm': + case 'M': + case 'h': + return input_format == input_format_t::bjdata; + default: + return false; + } + } + /*! @brief whether @a marker is excluded from BJData's optimized ND-array types @return whether @a marker is one of 'F', 'H', 'N', 'S', 'T', 'Z', '[', '{' diff --git a/tests/src/unit-bjdata.cpp b/tests/src/unit-bjdata.cpp index 67e9e0a05..4a34125ba 100644 --- a/tests/src/unit-bjdata.cpp +++ b/tests/src/unit-bjdata.cpp @@ -3134,7 +3134,7 @@ TEST_CASE("BJData") std::vector const v1 = {'[', '#', 'i', 0xF1}; std::vector const v2 = {'[', '$', 'I', '#', 'i', 0xF2}; std::vector const v3 = {'[', '$', 'I', '#', '[', 'i', 0xF4, 'i', 0x02, ']'}; - std::vector const v4 = {'[', '$', 0xF6, '#', 'i', 0xF7}; + std::vector const v4 = {'[', '$', 'l', '#', 'i', 0xF7}; std::vector const v5 = {'[', '$', 'I', '#', '[', 'i', 0xF5, 'i', 0xF1, ']'}; std::vector const v6 = {'[', '#', '[', 'i', 0xF3, 'i', 0x02, ']'}; @@ -3455,7 +3455,7 @@ TEST_CASE("BJData") CHECK(json::from_bjdata(v0, true, false).is_discarded()); std::vector const vi = {'[', '$', '#'}; - CHECK_THROWS_WITH_AS(_ = json::from_bjdata(vi), "[json.exception.parse_error.110] parse error at byte 4: syntax error while parsing BJData value: unexpected end of input", json::parse_error&); + CHECK_THROWS_WITH_AS(_ = json::from_bjdata(vi), "[json.exception.parse_error.112] parse error at byte 3: syntax error while parsing BJData type: marker 0x23 is not a permitted optimized array type", json::parse_error&); CHECK(json::from_bjdata(vi, true, false).is_discarded()); std::vector const vU = {'[', '$', 'U'}; @@ -4622,3 +4622,60 @@ TEST_CASE("BJData large strings and binaries (chunked reader)") } } } + +TEST_CASE("BJData optimized container type marker is validated for count 0") +{ + // the '$' type marker must be a legal marker even if no element follows + SECTION("invalid marker") + { + // 'Z', 'T', 'F', 'N', 'S', 'H', '[' and '{' are excluded in BJData + for (const char marker : + {'x', 'E', 'Z', 'T', 'F', 'N', 'S', 'H', '[', '{' + }) + { + for (const char open : + {'[', '{' + }) + { + for (const int count : + { + 0, 1 + }) + { + CAPTURE(marker) + CAPTURE(open) + CAPTURE(count) + const std::vector v = {static_cast(open), '$', static_cast(marker), '#', 'i', static_cast(count)}; + json _; + CHECK_THROWS_AS(_ = json::from_bjdata(v), json::parse_error&); + CHECK(json::from_bjdata(v, true, false).is_discarded()); + } + } + } + } + + SECTION("error message") + { + const std::vector v = {'[', '$', 'x', '#', 'i', 0}; + json _; + CHECK_THROWS_WITH_AS(_ = json::from_bjdata(v), "[json.exception.parse_error.112] parse error at byte 3: syntax error while parsing BJData type: marker 0x78 is not a permitted optimized array type", json::parse_error&); + } + + SECTION("valid marker") + { + for (const char marker : + {'i', 'U', 'I', 'u', 'l', 'm', 'L', 'M', 'h', 'd', 'D', 'C' + }) + { + CAPTURE(marker) + const std::vector va = {'[', '$', static_cast(marker), '#', 'i', 0}; + const std::vector vo = {'{', '$', static_cast(marker), '#', 'i', 0}; + CHECK(json::from_bjdata(va) == json::array()); + CHECK(json::from_bjdata(vo) == json::object()); + } + + // an optimized array of bytes is read as a binary value + const std::vector vb = {'[', '$', 'B', '#', 'i', 0}; + CHECK(json::from_bjdata(vb) == json::binary({})); + } +} diff --git a/tests/src/unit-ubjson.cpp b/tests/src/unit-ubjson.cpp index 4cce74dff..5c6378216 100644 --- a/tests/src/unit-ubjson.cpp +++ b/tests/src/unit-ubjson.cpp @@ -2023,7 +2023,7 @@ TEST_CASE("UBJSON") CHECK(json::from_ubjson(v0, true, false).is_discarded()); std::vector const vi = {'[', '$', '#'}; - CHECK_THROWS_WITH_AS(_ = json::from_ubjson(vi), "[json.exception.parse_error.110] parse error at byte 4: syntax error while parsing UBJSON value: unexpected end of input", json::parse_error&); + CHECK_THROWS_WITH_AS(_ = json::from_ubjson(vi), "[json.exception.parse_error.112] parse error at byte 3: syntax error while parsing UBJSON type: marker 0x23 is not a permitted optimized array type", json::parse_error&); CHECK(json::from_ubjson(vi, true, false).is_discarded()); std::vector const vT = {'[', '$', 'T'}; @@ -3348,3 +3348,55 @@ TEST_CASE("UBJSON large strings (chunked reader)") } } } + +TEST_CASE("UBJSON optimized container type marker is validated for count 0") +{ + // the '$' type marker must be a legal marker even if no element follows + SECTION("invalid marker") + { + for (const char marker : + {'x', 'E', 'B', 'u', 'm', 'M', 'h' + }) + { + for (const char open : + {'[', '{' + }) + { + for (const int count : + { + 0, 1 + }) + { + CAPTURE(marker) + CAPTURE(open) + CAPTURE(count) + const std::vector v = {static_cast(open), '$', static_cast(marker), '#', 'i', static_cast(count)}; + json _; + CHECK_THROWS_AS(_ = json::from_ubjson(v), json::parse_error&); + CHECK(json::from_ubjson(v, true, false).is_discarded()); + } + } + } + } + + SECTION("error message") + { + const std::vector v = {'[', '$', 'x', '#', 'i', 0}; + json _; + CHECK_THROWS_WITH_AS(_ = json::from_ubjson(v), "[json.exception.parse_error.112] parse error at byte 3: syntax error while parsing UBJSON type: marker 0x78 is not a permitted optimized array type", json::parse_error&); + } + + SECTION("valid marker") + { + for (const char marker : + {'Z', 'N', 'T', 'F', 'i', 'U', 'I', 'l', 'L', 'd', 'D', 'H', 'C', 'S', '[', '{' + }) + { + CAPTURE(marker) + const std::vector va = {'[', '$', static_cast(marker), '#', 'i', 0}; + const std::vector vo = {'{', '$', static_cast(marker), '#', 'i', 0}; + CHECK(json::from_ubjson(va) == json::array()); + CHECK(json::from_ubjson(vo) == json::object()); + } + } +} From dc768a7d5da7e789e85514510915a029bbbb5b8e Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Mon, 26 Oct 2026 18:25:46 +0100 Subject: [PATCH 10/18] Fix BSON binary subtype 0x02 and check the declared document size earlier (#5930) * Write and read the inner length of BSON binary subtype 0x02 The BSON specification defines the payload of the "binary (old)" subtype 0x02 as an int32 followed by that many bytes. to_bson wrote the plain payload instead, so decoders such as pymongo and libbson-based readers rejected the bytes, and from_bson returned the inner length as part of the payload (documented as lenient behavior in gh-5333). The writer now emits the outer length (payload + 4), the subtype, the inner int32 holding the payload size, and the payload. The reader requires an outer length of at least 4 and an inner length equal to the outer length minus 4 (the cases the bson-corpus lists as decode errors), reports parse error 112 otherwise, and returns only the payload. Both directions change together so that the round trip keeps working. The single-reservation fast path for scalar entries declines subtype 0x02 because it has no room for the inner length. This is a deliberate exception to the opt-in rule for behavior changes: the previous output was invalid BSON. Signed-off-by: Niels Lohmann * Check the declared BSON document size before reading its content The BSON reader compared a document's declared size with the number of bytes read only when it reached the terminator. The declared size never bounded the reads, so a SAX handler saw the keys and values of a document whose size was too small before parse error 112 was reported. The container frame now stores the position where the document ends according to its declared size. open_bson_document rejects a size below 5 (size prefix plus terminator) and an embedded document that does not end before the terminator of its enclosing document; the element loop rejects an element whose type byte leaves no room for the name terminator and the document terminator. The terminator check stays and still catches a declared size that is too large. No input that was accepted before is rejected now; documents with a wrong size are reported earlier, with fewer SAX events, a different byte position and, in three cases, a different message. The error id (parse_error.112) is unchanged and strict=false still does not skip the check. The tests that asserted the old messages and positions are updated, with the outer sizes of two inputs corrected so that they still exercise the embedded document. New tests cover the SAX events, a size below 5, and an embedded document that fits exactly. Signed-off-by: Niels Lohmann * Document BSON subtype 0x02, document size checks, and lenient reads Describe the inner length of binary subtype 0x02 that to_bson now writes and from_bson checks and removes. Add the document size checks (parse_error.112, not affected by strict=false) to bson.md and from_bson.md, and extend the list of lenient reads with the UUID and MD5 payload lengths (not checked), duplicate keys (last value wins), embedded NUL bytes in strings, and the ignored trailing bytes with strict=false. State that the members of an object are written in iteration order. Correct the statement that element names are never validated on read: they are checked and sanitized like string values by the error handler. Add the BSON message to the examples of out_of_range.415 and type_error.321, the subtype limit of 255 to the binary values page, CBOR to the formats that use byte_container_with_subtype, and fix a typo in the incomplete mapping box. Signed-off-by: Niels Lohmann * Regenerate single_include and apply astyle Result of make amalgamate (including make pretty) for the preceding changes. Signed-off-by: Niels Lohmann * Address review of the BSON documentation Describe exactly when a too small document size is detected and note that subtype 2 data written by earlier versions is not readable. Signed-off-by: Niels Lohmann * docs: use the Oxford comma, drop discarded-value remarks Add the serial comma in a list added by this PR and remove the BSON example of type_error.321 (with the type_error.317 remark) so the docs do not mention discarded values for BSON; restore the original singular heading of that section. No tables were changed by this PR. Signed-off-by: Niels Lohmann * Fix -Weffc++ in the BSON recorder test and a clang-tidy use-auto hit Signed-off-by: Niels Lohmann * Fix clang-tidy readability-redundant-member-init in BSON test Signed-off-by: Niels Lohmann --------- Signed-off-by: Niels Lohmann --- docs/mkdocs/docs/api/basic_json/from_bson.md | 3 +- docs/mkdocs/docs/api/basic_json/to_bson.md | 2 +- .../api/byte_container_with_subtype/index.md | 2 +- .../docs/features/binary_formats/bson.md | 46 ++- docs/mkdocs/docs/features/binary_values.md | 5 +- docs/mkdocs/docs/home/exceptions.md | 6 +- .../nlohmann/detail/input/binary_reader.hpp | 71 +++++ .../nlohmann/detail/output/binary_writer.hpp | 27 +- single_include/nlohmann/json.hpp | 98 ++++++- tests/src/unit-bson.cpp | 266 ++++++++++++++++-- 10 files changed, 491 insertions(+), 35 deletions(-) diff --git a/docs/mkdocs/docs/api/basic_json/from_bson.md b/docs/mkdocs/docs/api/basic_json/from_bson.md index 6e0beafce..c04933eef 100644 --- a/docs/mkdocs/docs/api/basic_json/from_bson.md +++ b/docs/mkdocs/docs/api/basic_json/from_bson.md @@ -80,7 +80,8 @@ Strong guarantee: if an exception is thrown, there are no changes in the JSON va - Throws [`parse_error.110`](../../home/exceptions.md#jsonexceptionparse_error110) if the given input ends prematurely or the end of the input was not reached when `strict` was set to true - Throws [`parse_error.112`](../../home/exceptions.md#jsonexceptionparse_error112) if a parse error occurs (e.g., an - invalid string or byte array length) + invalid string or byte array length, an invalid inner length of a binary value with subtype `0x02`, or a document size + that does not match the content; `strict` does not affect the document size checks) - Throws [`parse_error.114`](../../home/exceptions.md#jsonexceptionparse_error114) if an unsupported BSON record type is encountered - Throws [`parse_error.113`](../../home/exceptions.md#jsonexceptionparse_error113) if a string value or object key is diff --git a/docs/mkdocs/docs/api/basic_json/to_bson.md b/docs/mkdocs/docs/api/basic_json/to_bson.md index 315477e19..b3b346266 100644 --- a/docs/mkdocs/docs/api/basic_json/to_bson.md +++ b/docs/mkdocs/docs/api/basic_json/to_bson.md @@ -13,7 +13,7 @@ static void to_bson(const basic_json& j, detail::output_adapter o, ``` BSON (Binary JSON) is a binary format in which zero or more ordered key/value pairs are stored as a single entity (a -so-called document). +so-called document). The members of an object are written in the order in which iteration visits them. 1. Returns a byte vector containing the BSON serialization. 2. Writes the BSON serialization to an output adapter. diff --git a/docs/mkdocs/docs/api/byte_container_with_subtype/index.md b/docs/mkdocs/docs/api/byte_container_with_subtype/index.md index b184d8496..f3a2cc9a3 100644 --- a/docs/mkdocs/docs/api/byte_container_with_subtype/index.md +++ b/docs/mkdocs/docs/api/byte_container_with_subtype/index.md @@ -6,7 +6,7 @@ class byte_container_with_subtype : public BinaryType; ``` This type extends the template parameter `BinaryType` provided to [`basic_json`](../basic_json/index.md) with a subtype -used by BSON and MessagePack. This type exists so that the user does not have to specify a type themselves with a +used by BSON, CBOR, and MessagePack. This type exists so that the user does not have to specify a type themselves with a specific naming scheme in order to override the binary type. ## Template parameters diff --git a/docs/mkdocs/docs/features/binary_formats/bson.md b/docs/mkdocs/docs/features/binary_formats/bson.md index 9c82594dd..04aef6950 100644 --- a/docs/mkdocs/docs/features/binary_formats/bson.md +++ b/docs/mkdocs/docs/features/binary_formats/bson.md @@ -66,7 +66,12 @@ The library uses the following mapping from JSON values types to BSON types: !!! warning "Incomplete mapping" The mapping is **incomplete**, since only JSON-objects (and things contained therein) can be serialized to BSON. - Also, keys may not contain U+0000, since they are serialized a zero-terminated c-strings. + Also, keys may not contain U+0000, since they are serialized as zero-terminated C strings. + +!!! info "Order of object members" + + The members of an object are written in the order in which iteration visits them: sorted by key for + [`json`](../../api/json.md), in insertion order for [`ordered_json`](../../api/ordered_json.md). !!! warning "BSON type 0x11 interoperability" @@ -81,6 +86,19 @@ The library uses the following mapping from JSON values types to BSON types: As a result, serializing and deserializing a JSON object containing such a value produces a different JSON object, even though the binary data is unchanged. +!!! info "Binary subtype 0x02 (old binary)" + + The BSON specification defines the payload of the "binary (old)" subtype `0x02` as an int32 followed by that many + bytes, so the value is preceded by two lengths: the length of the binary element and, as the first four bytes of + the payload, the number of bytes that follow. A binary value with subtype `2` is therefore written with the outer + length 4 bytes larger than its size and the size as inner length: `binary({1, 2, 3}, 2)` becomes + `07 00 00 00 02 03 00 00 00 01 02 03`. When reading, the inner length is checked and removed, so the value + contains only the bytes that follow it. `from_bson()` throws + [`parse_error.112`](../../home/exceptions.md#jsonexceptionparse_error112) if the outer length is less than 4 or the + inner length is not the outer length minus 4. Versions before 3.13.0 did not write the inner length and returned + it as part of the payload; BSON data written by those versions with subtype 2 is not readable any more and must be + written again. + ??? example "Example: serialize a JSON value to BSON" ```cpp @@ -135,12 +153,29 @@ The library maps BSON record types to JSON value types as follows: The BSON reader is lenient in a few areas where the BSON specification is more restrictive: - array element keys are not checked against the required decimal sequence (`0`, `1`, `2`, ...), - - any non-zero byte is accepted as `true` for the boolean type, and - - the payload for binary subtype `0x02` is returned as-is, including its inner length prefix. + - any non-zero byte is accepted as `true` for the boolean type, + - the payloads of the binary subtypes `0x03` (UUID, old), `0x04` (UUID), and `0x05` (MD5) are not checked to have + 16 bytes, + - duplicate keys within a document are accepted; the last value wins (`{"a": 1, "a": 2}` is read as `{"a": 2}`), + while a SAX handler receives every occurrence, + - a `string` value may contain embedded `0x00` bytes, and + - with `strict` set to `false`, bytes after the end of the document are ignored. If BSON input must be validated for strict specification compliance, validate it separately before passing it to `from_bson()`. +!!! note "Document size" + + Every document starts with its size. `from_bson()` rejects a document with + [`parse_error.112`](../../home/exceptions.md#jsonexceptionparse_error112) if the size is less than 5 (the size prefix and the terminator), if an embedded document does not end before the + end of the document that contains it, if the elements do not fit into the declared size, or if the size does not + match the number of bytes read when the terminating `0x00` byte is reached. A size that is too small is detected + before an element is read whose type byte and name no longer fit; an element that starts inside the declared size + but extends past it is read, and reported to a SAX handler, before the mismatch is found at the terminator. A size + that is too large is likewise found only at the terminator. Passing `strict` as `false` skips only the check for bytes after + the end of the document ([`parse_error.110`](../../home/exceptions.md#jsonexceptionparse_error110)), not the + document size checks. + !!! warning "Ill-formed UTF-8 in string values" The BSON specification requires `string` values (type `0x02`) to be valid UTF-8, but this is not required of a @@ -154,8 +189,9 @@ The library maps BSON record types to JSON value types as follows: ill-formed bytes. `to_bson()`'s own `error_handler` parameter defaults to `keep`, so such a string value or element (key) name is written unchanged; with `strict` (the default if [`JSON_STRICT_BINARY_UTF8`](../../api/macros/json_strict_binary_utf8.md) is enabled), it throws the same exception - instead. Element (key) names are never validated on read, since they are read byte-by-byte as a C string. `binary` - values (type `0x05`) are unaffected, since they are not required to hold text. + instead. Element (key) names are treated like string values: with `keep` they are read unchanged, with `strict`, + `replace`, or `ignore` they are checked and handled in the same way. `binary` values (type `0x05`) are unaffected, + since they are not required to hold text. ??? example "Example: deserialize a JSON value from BSON" diff --git a/docs/mkdocs/docs/features/binary_values.md b/docs/mkdocs/docs/features/binary_values.md index f5a87472b..e4dee45d4 100644 --- a/docs/mkdocs/docs/features/binary_values.md +++ b/docs/mkdocs/docs/features/binary_values.md @@ -227,7 +227,10 @@ array of integers. ### BSON [BSON](binary_formats/bson.md) supports binary values and subtypes. If a subtype is given, it is used and added as an -unsigned 8-bit integer. If no subtype is given, the generic binary subtype 0x00 is used. +unsigned 8-bit integer; a subtype above 255 throws +[`out_of_range.415`](../home/exceptions.md#jsonexceptionout_of_range415). If no subtype is given, the generic binary +subtype 0x00 is used. The payload of the "old binary" subtype 0x02 is preceded by a second length, which the library +writes and removes again when reading; it is not part of the payload. ??? example "Example: serialize a binary value to BSON" diff --git a/docs/mkdocs/docs/home/exceptions.md b/docs/mkdocs/docs/home/exceptions.md index ddf9d69cd..48825cb25 100644 --- a/docs/mkdocs/docs/home/exceptions.md +++ b/docs/mkdocs/docs/home/exceptions.md @@ -1162,12 +1162,16 @@ A JSON Patch `move` operation's `"from"` location is a proper prefix of its `"pa MessagePack's ext type and BSON's binary subtype are each stored in a single byte. This exception is thrown when serializing a [`byte_container_with_subtype`](../api/byte_container_with_subtype/index.md) whose subtype exceeds 255, the largest value the library can store in that byte. -!!! failure "Example message" +!!! failure "Example messages" ``` [json.exception.out_of_range.415] subtype 70000 is too large for the MessagePack ext type (max 255) ``` + ``` + [json.exception.out_of_range.415] subtype 70000 is too large for the BSON binary subtype (max 255) + ``` + !!! note This exception was added in version 3.13.0. Before that, subtypes above 255 were silently truncated modulo 256 instead of raising an error. diff --git a/include/nlohmann/detail/input/binary_reader.hpp b/include/nlohmann/detail/input/binary_reader.hpp index 4c8d6ed0f..b44fd868c 100644 --- a/include/nlohmann/detail/input/binary_reader.hpp +++ b/include/nlohmann/detail/input/binary_reader.hpp @@ -235,6 +235,9 @@ class binary_reader /// UBJSON/BJData: the type marker of an optimized container, so that /// its elements are read without one of their own; 0 otherwise char_int_type type_marker; + /// BSON: the position right after this document, according to the + /// size it declares + std::size_t end_position = 0; /// BSON: the size this document declares, in bytes std::int32_t declared_size = 0; /// whether to close this container with end_object() or end_array() @@ -371,6 +374,23 @@ class binary_reader return false; } + // the smallest document is the size prefix and the terminator + if (JSON_HEDLEY_UNLIKELY(document_size < 5)) + { + return bson_document_size_error("document size ", document_size, " is less than 5"); + } + + // the declared size bounds where the document ends: an embedded + // document must end before the terminator of the document that contains it + const auto unsigned_size = static_cast(document_size); + const std::size_t document_end = unsigned_size > (std::numeric_limits::max)() - document_start + ? (std::numeric_limits::max)() + : document_start + unsigned_size; + if (JSON_HEDLEY_UNLIKELY(!container_stack.empty() && document_end >= container_stack.back().end_position)) + { + return bson_document_size_error("document size ", document_size, " exceeds the size of the enclosing document"); + } + if (JSON_HEDLEY_UNLIKELY(!enter_container(is_object, detail::unknown_size()))) { return false; @@ -378,6 +398,7 @@ class binary_reader container_frame& frame = container_stack.back(); frame.start_position = document_start; + frame.end_position = document_end; frame.declared_size = document_size; return true; } @@ -436,6 +457,13 @@ class binary_reader return false; } + // an element needs at least its name's terminator, and the + // document its own terminator, within the declared size + if (JSON_HEDLEY_UNLIKELY(chars_read + 2 > container_stack.back().end_position)) + { + return bson_document_size_error("document size ", container_stack.back().declared_size, " is too small for its content"); + } + const std::size_t element_type_parse_position = chars_read; key.clear(); if (JSON_HEDLEY_UNLIKELY(!get_bson_cstr(key))) @@ -584,9 +612,46 @@ class binary_reader } result.set_subtype(subtype); + // the "old binary" subtype 0x02 starts with an int32 that repeats the + // size of the payload following it; the payload does not include it + if (subtype == 0x02) + { + return get_bson_old_binary(len, result); + } + return get_binary(len, result); } + /*! + @brief Parses the payload of a BSON binary value with subtype 0x02. + @param[in] len The length of the binary value, including the inner int32. + @param[in,out] result A reference to the binary variable where the payload + (without the inner int32) is to be stored. + @pre len >= 0 + @return `true` if the inner length was present and matches @a len + */ + template + bool get_bson_old_binary(const NumberType len, binary_t& result) + { + if (JSON_HEDLEY_UNLIKELY(len < 4)) + { + return bson_length_error("length of binary subtype 0x02 must be at least 4, is ", len, "binary"); + } + + std::int32_t inner_length{}; + if (JSON_HEDLEY_UNLIKELY((!get_number(inner_length)))) + { + return false; + } + + if (JSON_HEDLEY_UNLIKELY(static_cast(inner_length) != static_cast(len) - 4)) + { + return bson_length_error("inner length of binary subtype 0x02 does not match the payload size; inner length is ", inner_length, "binary"); + } + + return get_binary(inner_length, result); + } + /*! @brief Read a BSON document element of the given @a element_type. @param[in] element_type The BSON element type, c.f. http://bsonspec.org/spec.html @@ -4399,6 +4464,12 @@ class binary_reader return last_byte_error(exception_id::unexpected_byte, concat("document size ", std::to_string(document_size), " does not match the number of bytes read (", std::to_string(bytes_read), ")"), "document"); } + JSON_HEDLEY_NEVER_INLINE + bool bson_document_size_error(const char* prefix, const std::int32_t document_size, const char* suffix) const + { + return last_byte_error(exception_id::unexpected_byte, concat(prefix, std::to_string(document_size), suffix), "document"); + } + JSON_HEDLEY_NEVER_INLINE bool bson_unsupported_type_error(const std::size_t position, const char_int_type element_type) const { diff --git a/include/nlohmann/detail/output/binary_writer.hpp b/include/nlohmann/detail/output/binary_writer.hpp index 7ff828074..d8b3e7162 100644 --- a/include/nlohmann/detail/output/binary_writer.hpp +++ b/include/nlohmann/detail/output/binary_writer.hpp @@ -1684,7 +1684,15 @@ class binary_writer throw_subtype_out_of_range(value.subtype(), "the BSON binary subtype", j); } - return sizeof(std::int32_t) + value.size() + 1ul; + // the "old binary" subtype 0x02 repeats the payload size in an inner + // int32 in front of the payload + return sizeof(std::int32_t) + (is_bson_old_binary(value) ? sizeof(std::int32_t) : 0ul) + value.size() + 1ul; + } + + /// @return whether @a value has the BSON subtype 0x02 ("binary (old)") + static bool is_bson_old_binary(const binary_t& value) noexcept + { + return value.has_subtype() && value.subtype() == 0x02; } /*! @@ -1697,10 +1705,18 @@ class binary_writer { write_bson_entry_header(name, 0x05); - write_number(to_bson_length(value.size()), true); + const bool old_binary = is_bson_old_binary(value); + + // the length field counts the inner int32 of the old binary subtype + write_number(to_bson_length(value.size() + (old_binary ? sizeof(std::int32_t) : 0ul)), true); write_number(value.has_subtype() ? static_cast(value.subtype()) : static_cast(0x00)); + if (old_binary) + { + write_number(to_bson_length(value.size()), true); + } + write_bytes(value.data(), value.size()); } @@ -1860,7 +1876,8 @@ class binary_writer (or the header of a string or binary value) in one reservation @return whether the entry was written; false for a discarded value, a string or binary value whose length does not fit the length - field, in which case nothing was written + field, or a binary value with subtype 0x02, in which case nothing + was written @pre @a name is at most bson_scalar_max_name_size characters long and is not changed by the error handler */ @@ -1921,7 +1938,9 @@ class binary_writer if (j.is_binary()) { const binary_t& value = *j.m_data.m_value.binary; - if (JSON_HEDLEY_UNLIKELY(!value_in_range_of(value.size()))) + // the old binary subtype 0x02 has an inner length that the single + // reservation has no room for; write_bson_binary writes it + if (JSON_HEDLEY_UNLIKELY(!value_in_range_of(value.size()) || is_bson_old_binary(value))) { return false; } diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index c4b7f159a..b67223f0c 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -17702,6 +17702,9 @@ class binary_reader /// UBJSON/BJData: the type marker of an optimized container, so that /// its elements are read without one of their own; 0 otherwise char_int_type type_marker; + /// BSON: the position right after this document, according to the + /// size it declares + std::size_t end_position = 0; /// BSON: the size this document declares, in bytes std::int32_t declared_size = 0; /// whether to close this container with end_object() or end_array() @@ -17838,6 +17841,23 @@ class binary_reader return false; } + // the smallest document is the size prefix and the terminator + if (JSON_HEDLEY_UNLIKELY(document_size < 5)) + { + return bson_document_size_error("document size ", document_size, " is less than 5"); + } + + // the declared size bounds where the document ends: an embedded + // document must end before the terminator of the document that contains it + const auto unsigned_size = static_cast(document_size); + const std::size_t document_end = unsigned_size > (std::numeric_limits::max)() - document_start + ? (std::numeric_limits::max)() + : document_start + unsigned_size; + if (JSON_HEDLEY_UNLIKELY(!container_stack.empty() && document_end >= container_stack.back().end_position)) + { + return bson_document_size_error("document size ", document_size, " exceeds the size of the enclosing document"); + } + if (JSON_HEDLEY_UNLIKELY(!enter_container(is_object, detail::unknown_size()))) { return false; @@ -17845,6 +17865,7 @@ class binary_reader container_frame& frame = container_stack.back(); frame.start_position = document_start; + frame.end_position = document_end; frame.declared_size = document_size; return true; } @@ -17903,6 +17924,13 @@ class binary_reader return false; } + // an element needs at least its name's terminator, and the + // document its own terminator, within the declared size + if (JSON_HEDLEY_UNLIKELY(chars_read + 2 > container_stack.back().end_position)) + { + return bson_document_size_error("document size ", container_stack.back().declared_size, " is too small for its content"); + } + const std::size_t element_type_parse_position = chars_read; key.clear(); if (JSON_HEDLEY_UNLIKELY(!get_bson_cstr(key))) @@ -18051,9 +18079,46 @@ class binary_reader } result.set_subtype(subtype); + // the "old binary" subtype 0x02 starts with an int32 that repeats the + // size of the payload following it; the payload does not include it + if (subtype == 0x02) + { + return get_bson_old_binary(len, result); + } + return get_binary(len, result); } + /*! + @brief Parses the payload of a BSON binary value with subtype 0x02. + @param[in] len The length of the binary value, including the inner int32. + @param[in,out] result A reference to the binary variable where the payload + (without the inner int32) is to be stored. + @pre len >= 0 + @return `true` if the inner length was present and matches @a len + */ + template + bool get_bson_old_binary(const NumberType len, binary_t& result) + { + if (JSON_HEDLEY_UNLIKELY(len < 4)) + { + return bson_length_error("length of binary subtype 0x02 must be at least 4, is ", len, "binary"); + } + + std::int32_t inner_length{}; + if (JSON_HEDLEY_UNLIKELY((!get_number(inner_length)))) + { + return false; + } + + if (JSON_HEDLEY_UNLIKELY(static_cast(inner_length) != static_cast(len) - 4)) + { + return bson_length_error("inner length of binary subtype 0x02 does not match the payload size; inner length is ", inner_length, "binary"); + } + + return get_binary(inner_length, result); + } + /*! @brief Read a BSON document element of the given @a element_type. @param[in] element_type The BSON element type, c.f. http://bsonspec.org/spec.html @@ -21866,6 +21931,12 @@ class binary_reader return last_byte_error(exception_id::unexpected_byte, concat("document size ", std::to_string(document_size), " does not match the number of bytes read (", std::to_string(bytes_read), ")"), "document"); } + JSON_HEDLEY_NEVER_INLINE + bool bson_document_size_error(const char* prefix, const std::int32_t document_size, const char* suffix) const + { + return last_byte_error(exception_id::unexpected_byte, concat(prefix, std::to_string(document_size), suffix), "document"); + } + JSON_HEDLEY_NEVER_INLINE bool bson_unsupported_type_error(const std::size_t position, const char_int_type element_type) const { @@ -28045,7 +28116,15 @@ class binary_writer throw_subtype_out_of_range(value.subtype(), "the BSON binary subtype", j); } - return sizeof(std::int32_t) + value.size() + 1ul; + // the "old binary" subtype 0x02 repeats the payload size in an inner + // int32 in front of the payload + return sizeof(std::int32_t) + (is_bson_old_binary(value) ? sizeof(std::int32_t) : 0ul) + value.size() + 1ul; + } + + /// @return whether @a value has the BSON subtype 0x02 ("binary (old)") + static bool is_bson_old_binary(const binary_t& value) noexcept + { + return value.has_subtype() && value.subtype() == 0x02; } /*! @@ -28058,10 +28137,18 @@ class binary_writer { write_bson_entry_header(name, 0x05); - write_number(to_bson_length(value.size()), true); + const bool old_binary = is_bson_old_binary(value); + + // the length field counts the inner int32 of the old binary subtype + write_number(to_bson_length(value.size() + (old_binary ? sizeof(std::int32_t) : 0ul)), true); write_number(value.has_subtype() ? static_cast(value.subtype()) : static_cast(0x00)); + if (old_binary) + { + write_number(to_bson_length(value.size()), true); + } + write_bytes(value.data(), value.size()); } @@ -28221,7 +28308,8 @@ class binary_writer (or the header of a string or binary value) in one reservation @return whether the entry was written; false for a discarded value, a string or binary value whose length does not fit the length - field, in which case nothing was written + field, or a binary value with subtype 0x02, in which case nothing + was written @pre @a name is at most bson_scalar_max_name_size characters long and is not changed by the error handler */ @@ -28282,7 +28370,9 @@ class binary_writer if (j.is_binary()) { const binary_t& value = *j.m_data.m_value.binary; - if (JSON_HEDLEY_UNLIKELY(!value_in_range_of(value.size()))) + // the old binary subtype 0x02 has an inner length that the single + // reservation has no room for; write_bson_binary writes it + if (JSON_HEDLEY_UNLIKELY(!value_in_range_of(value.size()) || is_bson_old_binary(value))) { return false; } diff --git a/tests/src/unit-bson.cpp b/tests/src/unit-bson.cpp index 03c901cee..b4756c9b5 100644 --- a/tests/src/unit-bson.cpp +++ b/tests/src/unit-bson.cpp @@ -802,12 +802,11 @@ TEST_CASE("BSON") CHECK(json::from_bson(result, true, false) == j); } - SECTION("binary member with subtype 0x02 (old binary) keeps its inner length prefix (lenient parsing)") + SECTION("binary member with subtype 0x02 (old binary) carries an inner length") { - // documented lenient behavior (see gh-5333): the payload for - // binary subtype 0x02 ("old binary") is returned as-is, - // including its own inner 4-byte length prefix; it is not - // stripped or reinterpreted + // the payload of binary subtype 0x02 ("old binary") starts with an + // int32 that repeats the payload size; the bytes below are what + // pymongo and the bson-corpus (binary.json) use std::vector const input = { 0x17, 0x00, 0x00, 0x00, // size (little endian) @@ -815,16 +814,87 @@ TEST_CASE("BSON") 0x06, 0x00, 0x00, 0x00, // size of binary (little endian) 0x02, // "old binary" subtype - 0x02, 0x00, 0x00, 0x00, // inner length prefix (part of the old-binary payload) + 0x02, 0x00, 0x00, 0x00, // inner length 0x68, 0x69, // payload ('h', 'i') 0x00 // end marker }; - // the inner length prefix is part of the (unmodified) payload - const std::vector expected_payload = {0x02, 0x00, 0x00, 0x00, 0x68, 0x69}; - const json expected = { { "entry", json::binary(expected_payload, 0x02) } }; + // the inner length is not part of the payload + const json expected = { { "entry", json::binary({0x68, 0x69}, 0x02) } }; CHECK(json::from_bson(input) == expected); + CHECK(json::to_bson(expected) == input); + CHECK(json::from_bson(json::to_bson(expected)) == expected); + } + + SECTION("binary member with subtype 0x02 (old binary) and empty payload") + { + std::vector const input = + { + 0x15, 0x00, 0x00, 0x00, // size (little endian) + 0x05, 'e', 'n', 't', 'r', 'y', '\x00', // entry: binary + 0x04, 0x00, 0x00, 0x00, // size of binary (little endian) + 0x02, // "old binary" subtype + 0x00, 0x00, 0x00, 0x00, // inner length + 0x00 // end marker + }; + + const json expected = { { "entry", json::binary({}, 0x02) } }; + CHECK(json::from_bson(input) == expected); + CHECK(json::to_bson(expected) == input); + } + + SECTION("binary member with subtype 0x02 (old binary) with an invalid inner length") + { + std::vector input = + { + 0x17, 0x00, 0x00, 0x00, // size (little endian) + 0x05, 'e', 'n', 't', 'r', 'y', '\x00', // entry: binary + 0x06, 0x00, 0x00, 0x00, // size of binary (little endian) + 0x02, // "old binary" subtype + 0x02, 0x00, 0x00, 0x00, // inner length + 0x68, 0x69, // payload ('h', 'i') + 0x00 // end marker + }; + + SECTION("inner length too long") + { + input[19] = 0x03; + } + + SECTION("inner length too short") + { + input[19] = 0x01; + } + + SECTION("inner length negative") + { + input[19] = 0xFF; + input[20] = 0xFF; + input[21] = 0xFF; + input[22] = 0xFF; + } + + CAPTURE(input) + json _; + CHECK_THROWS_AS(_ = json::from_bson(input), json::parse_error&); + } + + SECTION("binary member with subtype 0x02 (old binary) without an inner length") + { + // the payload is shorter than the inner length field + std::vector const input = + { + 0x14, 0x00, 0x00, 0x00, // size (little endian) + 0x05, 'e', 'n', 't', 'r', 'y', '\x00', // entry: binary + 0x03, 0x00, 0x00, 0x00, // size of binary (little endian) + 0x02, // "old binary" subtype + 0x01, 0x02, 0x03, // payload without an inner length + 0x00 // end marker + }; + + json _; + CHECK_THROWS_AS(_ = json::from_bson(input), json::parse_error&); } SECTION("Some more complex document") @@ -1178,7 +1248,7 @@ TEST_CASE("BSON document size mismatch") SECTION("top-level document with a negative size") { std::vector const input = {0xFF, 0xFF, 0xFF, 0xFF, 0x00}; - CHECK_THROWS_WITH_AS(_ = json::from_bson(input), "[json.exception.parse_error.112] parse error at byte 5: syntax error while parsing BSON document: document size -1 does not match the number of bytes read (5)", json::parse_error&); + CHECK_THROWS_WITH_AS(_ = json::from_bson(input), "[json.exception.parse_error.112] parse error at byte 4: syntax error while parsing BSON document: document size -1 is less than 5", json::parse_error&); CHECK(json::from_bson(input, true, false).is_discarded()); } @@ -1190,7 +1260,7 @@ TEST_CASE("BSON document size mismatch") // enclosing document instead of "d" std::vector const input = { - 0x00, 0x00, 0x00, 0x00, // outer size + 0x14, 0x00, 0x00, 0x00, // outer size 0x03, 'd', 0x00, // entry: embedded document "d" 0xFF, 0xFF, 0xFF, 0x7F, // embedded size 0x7FFFFFFF 0x0A, 'a', 0x00, // entry: null "a" @@ -1198,7 +1268,24 @@ TEST_CASE("BSON document size mismatch") 0x08, 'h', 0x00, 0x01, // entry: bool "h" = true 0x00 // outer end marker }; - CHECK_THROWS_WITH_AS(_ = json::from_bson(input), "[json.exception.parse_error.112] parse error at byte 15: syntax error while parsing BSON document: document size 2147483647 does not match the number of bytes read (8)", json::parse_error&); + CHECK_THROWS_WITH_AS(_ = json::from_bson(input), "[json.exception.parse_error.112] parse error at byte 11: syntax error while parsing BSON document: document size 2147483647 exceeds the size of the enclosing document", json::parse_error&); + CHECK(json::from_bson(input, true, false).is_discarded()); + } + + SECTION("embedded document whose size is smaller than its content") + { + // {"a":null} is 8 bytes, but the length prefix claims 7 + std::vector const input = + { + 0x14, 0x00, 0x00, 0x00, // outer size + 0x03, 'd', 0x00, // entry: embedded document "d" + 0x07, 0x00, 0x00, 0x00, // embedded size 7 (real is 8) + 0x0A, 'a', 0x00, // entry: null "a" + 0x00, // embedded end marker + 0x08, 'h', 0x00, 0x01, // entry: bool "h" = true + 0x00 // outer end marker + }; + CHECK_THROWS_WITH_AS(_ = json::from_bson(input), "[json.exception.parse_error.112] parse error at byte 15: syntax error while parsing BSON document: document size 7 does not match the number of bytes read (8)", json::parse_error&); CHECK(json::from_bson(input, true, false).is_discarded()); } @@ -1207,18 +1294,163 @@ TEST_CASE("BSON document size mismatch") // array [42] is 12 bytes, but the length prefix claims 13 std::vector const input = { - 0x00, 0x00, 0x00, 0x00, // outer size + 0x14, 0x00, 0x00, 0x00, // outer size 0x04, 'a', 0x00, // entry: array "a" 0x0D, 0x00, 0x00, 0x00, // array size 13 (real is 12) 0x10, '0', 0x00, 0x2A, 0x00, 0x00, 0x00, // entry: int32 "0" = 42 0x00, // array end marker 0x00 // outer end marker }; - CHECK_THROWS_WITH_AS(_ = json::from_bson(input), "[json.exception.parse_error.112] parse error at byte 19: syntax error while parsing BSON document: document size 13 does not match the number of bytes read (12)", json::parse_error&); + CHECK_THROWS_WITH_AS(_ = json::from_bson(input), "[json.exception.parse_error.112] parse error at byte 11: syntax error while parsing BSON document: document size 13 exceeds the size of the enclosing document", json::parse_error&); CHECK(json::from_bson(input, true, false).is_discarded()); } } +TEST_CASE("BSON declared document size bounds the content") +{ + // records the events that a SAX parser receives + struct recorder : nlohmann::json_sax + { + std::string events; + + bool null() override + { + events += "null "; + return true; + } + bool boolean(bool /*val*/) override + { + events += "boolean "; + return true; + } + bool number_integer(json::number_integer_t /*val*/) override + { + events += "integer "; + return true; + } + bool number_unsigned(json::number_unsigned_t /*val*/) override + { + events += "unsigned "; + return true; + } + bool number_float(json::number_float_t /*val*/, const std::string& /*s*/) override + { + events += "float "; + return true; + } + bool string(std::string& /*val*/) override + { + events += "string "; + return true; + } + bool binary(json::binary_t& /*val*/) override + { + events += "binary "; + return true; + } + bool start_object(std::size_t /*elements*/) override + { + events += "{ "; + return true; + } + bool key(std::string& /*val*/) override + { + events += "key "; + return true; + } + bool end_object() override + { + events += "} "; + return true; + } + bool start_array(std::size_t /*elements*/) override + { + events += "[ "; + return true; + } + bool end_array() override + { + events += "] "; + return true; + } + bool parse_error(std::size_t /*position*/, const std::string& /*last_token*/, const json::exception& /*ex*/) override + { + events += "error "; + return false; + } + }; + + SECTION("the content of a too small document is not reported") + { + // {"a":1} is 12 bytes; the length prefix claims 5, so the element + // does not fit + std::vector const input = + { + 0x05, 0x00, 0x00, 0x00, + 0x10, 'a', 0x00, 0x01, 0x00, 0x00, 0x00, + 0x00 + }; + recorder rec; + CHECK(!json::sax_parse(input, &rec, json::input_format_t::bson)); + CHECK(rec.events == "{ error "); + + // also without strict mode + recorder rec2; + CHECK(!json::sax_parse(input, &rec2, json::input_format_t::bson, false)); + CHECK(rec2.events == "{ error "); + } + + SECTION("an embedded document that does not fit its parent is not opened") + { + // the embedded document ends where the enclosing one does, so there + // is no room for the terminator of the enclosing document + std::vector const input = + { + 0x0C, 0x00, 0x00, 0x00, + 0x03, 'd', 0x00, + 0x05, 0x00, 0x00, 0x00, 0x00, + 0x00 + }; + recorder rec; + CHECK(!json::sax_parse(input, &rec, json::input_format_t::bson)); + CHECK(rec.events == "{ key error "); + } + + SECTION("an embedded document that fits exactly") + { + std::vector const input = + { + 0x0D, 0x00, 0x00, 0x00, + 0x03, 'd', 0x00, + 0x05, 0x00, 0x00, 0x00, 0x00, + 0x00 + }; + CHECK(json::from_bson(input) == json({{"d", json::object()}})); + } + + SECTION("a document size that is less than 5") + { + for (const std::int32_t size : + { + 0, 1, 4, -1, (std::numeric_limits::min)() + }) + { + CAPTURE(size) + std::vector input = + { + 0x00, 0x00, 0x00, 0x00, 0x00 + }; + input[0] = static_cast(size & 0xFF); + input[1] = static_cast((size >> 8) & 0xFF); + input[2] = static_cast((size >> 16) & 0xFF); + input[3] = static_cast((size >> 24) & 0xFF); + json _; + CHECK_THROWS_AS(_ = json::from_bson(input), json::parse_error&); + CHECK(json::from_bson(input, true, false).is_discarded()); + } + } +} + TEST_CASE("BSON nesting does not consume the call stack") { // An embedded document or array used to be read by calling back into the @@ -1923,7 +2155,7 @@ TEST_CASE("Invalid document size handling") { std::vector const v = {0x04, 0x00, 0x00, 0x00, 0x00}; json _; - CHECK_THROWS_WITH_AS(_ = json::from_bson(v), "[json.exception.parse_error.112] parse error at byte 5: syntax error while parsing BSON document: document size 4 does not match the number of bytes read (5)", json::parse_error&); + CHECK_THROWS_WITH_AS(_ = json::from_bson(v), "[json.exception.parse_error.112] parse error at byte 4: syntax error while parsing BSON document: document size 4 is less than 5", json::parse_error&); CHECK(json::from_bson(v, true, false).is_discarded()); } @@ -1938,7 +2170,7 @@ TEST_CASE("Invalid document size handling") 0x00 }; json _; - CHECK_THROWS_WITH_AS(_ = json::from_bson(v), "[json.exception.parse_error.112] parse error at byte 16: syntax error while parsing BSON document: document size 5 does not match the number of bytes read (16)", json::parse_error&); + CHECK_THROWS_WITH_AS(_ = json::from_bson(v), "[json.exception.parse_error.112] parse error at byte 5: syntax error while parsing BSON document: document size 5 is too small for its content", json::parse_error&); CHECK(json::from_bson(v, true, false).is_discarded()); } @@ -1969,7 +2201,7 @@ TEST_CASE("Invalid document size handling") 0x00 // object terminator }; json _; - CHECK_THROWS_WITH_AS(_ = json::from_bson(v), "[json.exception.parse_error.112] parse error at byte 19: syntax error while parsing BSON document: document size 5 does not match the number of bytes read (12)", json::parse_error&); + CHECK_THROWS_WITH_AS(_ = json::from_bson(v), "[json.exception.parse_error.112] parse error at byte 12: syntax error while parsing BSON document: document size 5 is too small for its content", json::parse_error&); CHECK(json::from_bson(v, true, false).is_discarded()); } From 60107eda3fe346097ce5b2c065b2854a688de909 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Mon, 26 Oct 2026 18:25:46 +0100 Subject: [PATCH 11/18] Read BJData ND-arrays of float16, restrict ND-array char to 0..127, correct BJData docs (#5931) * Read ND-arrays of half floats and enforce the char limit in ND-arrays BJData N-dimensional arrays with element type 'h' (float16) failed to read with a misleading parse_error.112, because the reader's dtype name table lacked 'h'. It now maps to "half", the JData name. The writer still does not pack "half"; such an object is written as a plain object and reads back equal. BJData limits 'C' (char) to 0..127 in Draft 2 and Draft 3, and the 1-D reader enforces this, but ND-arrays with _ArrayType_ "char" wrote values up to 255 as 'C' elements and read them back without a check. The writer now only packs values in 0..127 (others fall back to a plain object that round-trips), and the reader rejects 'C' elements above 127 with parse_error.113 like the 1-D reader. Signed-off-by: Niels Lohmann * docs: correct and complete the BJData documentation Verify the BJData chapter against the library and the Draft 2/3/4 specifications and fix what did not match: - bjdata.md: correct the list of unused markers (N, not Z), the ND-array example (3x2, not 2x3), the conditions under which an object is packed as an ND-array (exactly three keys, row vectors, float-only elements for single/double, char 0..127, byte only in Draft 3), the binary table row and notes (which flag/draft combinations write [$B), the "$" type restriction (Draft 3, Z is excluded too), the H marker in the deserialization table, and the claim of a complete deserialization mapping. Document the supported drafts, that Draft 4 (SoA containers, extension type E) and column-major ND-arrays are rejected, ND-arrays of half floats, duplicate object keys, and link the Draft 3 specifications instead of master. - Add an API page for basic_json::bjdata_version_t with an example, and link it from the nav, the API index and to_bjdata. - Update binary_formats/index.md and binary_values.md, which still claimed that BJData has no binary type, and fix a wrong byte comment there. - Document parse_error.115 and the out_of_range.408 cases for from_bjdata, the precise other_error.502 condition for to_bjdata, and reword the note on truncated unsigned integers in exceptions.md (only ND-array elements were truncated before 3.13.0). Signed-off-by: Niels Lohmann * Regenerate single_include and apply astyle Result of make amalgamate (including make pretty) for the preceding changes. Signed-off-by: Niels Lohmann * Address review of the BJData documentation The $ type restriction exists since Draft 2 (Draft 3 added B), $ requires a count, ND char above 127 is rejected on read, and the new API page is listed in alphabetical order in the nav. Signed-off-by: Niels Lohmann * docs: format tables, use the Oxford comma Reformat the tables changed by this PR (BJData serialization and deserialization mappings, completeness and binary-value tables) so all rows are aligned, and add the Oxford comma to lists of three or more items in the added text. Signed-off-by: Niels Lohmann --------- Signed-off-by: Niels Lohmann --- .../docs/api/basic_json/bjdata_version_t.md | 48 ++++ .../mkdocs/docs/api/basic_json/from_bjdata.md | 5 +- docs/mkdocs/docs/api/basic_json/index.md | 1 + docs/mkdocs/docs/api/basic_json/to_bjdata.md | 7 +- .../mkdocs/docs/examples/bjdata_version_t.cpp | 33 +++ .../docs/examples/bjdata_version_t.output | 4 + .../docs/features/binary_formats/bjdata.md | 214 +++++++++++------- .../docs/features/binary_formats/index.md | 32 +-- docs/mkdocs/docs/features/binary_values.md | 11 +- docs/mkdocs/docs/home/exceptions.md | 14 +- docs/mkdocs/mkdocs.yml | 1 + .../nlohmann/detail/input/binary_reader.hpp | 22 ++ .../nlohmann/detail/output/binary_writer.hpp | 7 +- single_include/nlohmann/json.hpp | 29 ++- tests/src/unit-bjdata.cpp | 67 ++++++ 15 files changed, 380 insertions(+), 115 deletions(-) create mode 100644 docs/mkdocs/docs/api/basic_json/bjdata_version_t.md create mode 100644 docs/mkdocs/docs/examples/bjdata_version_t.cpp create mode 100644 docs/mkdocs/docs/examples/bjdata_version_t.output diff --git a/docs/mkdocs/docs/api/basic_json/bjdata_version_t.md b/docs/mkdocs/docs/api/basic_json/bjdata_version_t.md new file mode 100644 index 000000000..72eece5e5 --- /dev/null +++ b/docs/mkdocs/docs/api/basic_json/bjdata_version_t.md @@ -0,0 +1,48 @@ +# nlohmann::basic_json::bjdata_version_t + +```cpp +enum class bjdata_version_t +{ + draft2, + draft3 +}; +``` + +This enumeration is used in [`to_bjdata`](to_bjdata.md) to choose the version of the BJData specification to write: + +draft2 +: BJData Draft 2 (the default). Binary values are written as arrays of `U` integers, and ND-arrays with + `"_ArrayType_": "byte"` are written as regular objects, since the `B` marker is not part of Draft 2. + +draft3 +: BJData Draft 3. Binary values are written as `B` elements: as an optimized binary array (`[$B#`) if both `use_size` + and `use_type` are set, and as an array of `B`-prefixed elements otherwise. ND-arrays with + `"_ArrayType_": "byte"` are written as packed arrays (`[$B#[...]`). + +Only the writer is affected by the version. [`from_bjdata`](from_bjdata.md) reads the markers of both drafts. + +## Examples + +??? example + + The example below shows how the different values of the `bjdata_version_t` influence the serialization of a binary + value with [`to_bjdata`](to_bjdata.md). + + ```cpp + --8<-- "examples/bjdata_version_t.cpp" + ``` + + Output: + + ```json + --8<-- "examples/bjdata_version_t.output" + ``` + +## See also + +- [to_bjdata](to_bjdata.md) serializes a JSON value to BJData +- [BJData](../../features/binary_formats/bjdata.md) - the article on the BJData format + +## Version history + +- Added in version 3.12.0. diff --git a/docs/mkdocs/docs/api/basic_json/from_bjdata.md b/docs/mkdocs/docs/api/basic_json/from_bjdata.md index 370124c8a..2b697711c 100644 --- a/docs/mkdocs/docs/api/basic_json/from_bjdata.md +++ b/docs/mkdocs/docs/api/basic_json/from_bjdata.md @@ -82,8 +82,11 @@ Strong guarantee: if an exception is thrown, there are no changes in the JSON va - Throws [parse_error.112](../../home/exceptions.md#jsonexceptionparse_error112) if a parse error occurs - Throws [parse_error.113](../../home/exceptions.md#jsonexceptionparse_error113) if a string could not be parsed successfully, or if a string value or object key is not valid UTF-8 and `error_handler` is `strict` +- Throws [parse_error.115](../../home/exceptions.md#jsonexceptionparse_error115) if the text of a high-precision + number (`H`) is not a valid number - Throws [out_of_range.408](../../home/exceptions.md#jsonexceptionout_of_range408) if the size of an optimized container - or n-dimensional array cannot be represented by `std::size_t` + cannot be represented by `std::size_t` or exceeds the container's `max_size()`, or if the product of the dimensions of + an n-dimensional array overflows ## Complexity diff --git a/docs/mkdocs/docs/api/basic_json/index.md b/docs/mkdocs/docs/api/basic_json/index.md index acb67401f..96745922f 100644 --- a/docs/mkdocs/docs/api/basic_json/index.md +++ b/docs/mkdocs/docs/api/basic_json/index.md @@ -121,6 +121,7 @@ The class satisfies the following concept requirements: - [**json_pointer**](../json_pointer/index.md) - JSON Pointer implementation - [**json_serializer**](json_serializer.md) - type of the serializer to for conversions from/to JSON - [**error_handler_t**](error_handler_t.md) - type to choose behavior on decoding errors +- [**bjdata_version_t**](bjdata_version_t.md) - type to choose the BJData version to write - [**cbor_tag_handler_t**](cbor_tag_handler_t.md) - type to choose how to handle CBOR tags - **initializer_list_t** - type for initializer lists of `basic_json` values - [**input_format_t**](input_format_t.md) - type to choose the format to parse diff --git a/docs/mkdocs/docs/api/basic_json/to_bjdata.md b/docs/mkdocs/docs/api/basic_json/to_bjdata.md index a85d8ff8a..e0728cfdd 100644 --- a/docs/mkdocs/docs/api/basic_json/to_bjdata.md +++ b/docs/mkdocs/docs/api/basic_json/to_bjdata.md @@ -43,7 +43,8 @@ The exact mapping and its limitations are described on a [dedicated page](../../ `#!cpp false` by default. `version` (in) -: which version of BJData to use (see note on "Binary values" on [BJData](../../features/binary_formats/bjdata.md)); +: which version of BJData to use, see [`bjdata_version_t`](bjdata_version_t.md) and the note on "Binary values" on +[BJData](../../features/binary_formats/bjdata.md); optional, `#!cpp bjdata_version_t::draft2` by default. `error_handler` (in) @@ -64,7 +65,9 @@ Strong guarantee: if an exception is thrown, there are no changes in the JSON va ## Exceptions - Throws [`other_error.502`](../../home/exceptions.md#jsonexceptionother_error502) if `use_type` is true and `use_size` - is false, and `j` contains a non-empty array, object, or binary value. + is false, and `j` contains a non-empty array or object, or a non-empty binary value (with + `version = bjdata_version_t::draft3`, any binary value, including an empty one). + Scalars, empty arrays, and empty objects do not throw. - Throws [type_error.316](../../home/exceptions.md#jsonexceptiontype_error316) if a string or object key in `j` is not valid UTF-8 and `error_handler` is `strict` (the default only if [`JSON_STRICT_BINARY_UTF8`](../macros/json_strict_binary_utf8.md) is enabled) diff --git a/docs/mkdocs/docs/examples/bjdata_version_t.cpp b/docs/mkdocs/docs/examples/bjdata_version_t.cpp new file mode 100644 index 000000000..77660a0a1 --- /dev/null +++ b/docs/mkdocs/docs/examples/bjdata_version_t.cpp @@ -0,0 +1,33 @@ +#include +#include +#include + +using json = nlohmann::json; + +void print_hex(const char* label, const std::vector& v) +{ + std::cout << label << ':'; + for (auto b : v) + { + std::cout << ' ' << std::hex << std::setw(2) << std::setfill('0') << static_cast(b); + } + std::cout << std::dec << std::setfill(' ') << '\n'; +} + +int main() +{ + // a binary value + json j = json::binary({0xCA, 0xFE, 0xBA, 0xBE}); + + // Draft 2 (the default) writes an array of integers + auto v2 = json::to_bjdata(j, true, true, json::bjdata_version_t::draft2); + print_hex("draft2", v2); + + // Draft 3 writes an optimized binary array + auto v3 = json::to_bjdata(j, true, true, json::bjdata_version_t::draft3); + print_hex("draft3", v3); + + // reading yields an array for Draft 2 and a binary value for Draft 3 + std::cout << json::from_bjdata(v2) << '\n'; + std::cout << json::from_bjdata(v3) << '\n'; +} diff --git a/docs/mkdocs/docs/examples/bjdata_version_t.output b/docs/mkdocs/docs/examples/bjdata_version_t.output new file mode 100644 index 000000000..a37322f1c --- /dev/null +++ b/docs/mkdocs/docs/examples/bjdata_version_t.output @@ -0,0 +1,4 @@ +draft2: 5b 24 55 23 69 04 ca fe ba be +draft3: 5b 24 42 23 69 04 ca fe ba be +[202,254,186,190] +{"bytes":[202,254,186,190],"subtype":null} diff --git a/docs/mkdocs/docs/features/binary_formats/bjdata.md b/docs/mkdocs/docs/features/binary_formats/bjdata.md index 3a7a1798a..bc0599fcc 100644 --- a/docs/mkdocs/docs/features/binary_formats/bjdata.md +++ b/docs/mkdocs/docs/features/binary_formats/bjdata.md @@ -2,9 +2,9 @@ The [BJData format](https://neurojson.org) was derived from and improved upon [Universal Binary JSON(UBJSON)](https://ubjson.org) specification (Draft 12). Specifically, it introduces an optimized -array container for efficient storage of N-dimensional packed arrays (**ND-arrays**); it also adds 5 new type markers - -`[u] - uint16`, `[m] - uint32`, `[M] - uint64`, `[h] - float16` and `[B] - byte` - to unambiguously map common binary -numeric types; furthermore, it uses little-endian (LE) to store all numerics instead of big-endian (BE) as in UBJSON to +array container for efficient storage of N-dimensional packed arrays (**ND-arrays**); it also adds four new type markers - +`[u] - uint16`, `[m] - uint32`, `[M] - uint64`, and `[h] - float16` - to unambiguously map common binary numeric types +(Draft 3 adds `[B] - byte`); furthermore, it uses little-endian (LE) to store all numerics instead of big-endian (BE) as in UBJSON to avoid unnecessary conversions on commonly available platforms. Compared to other binary JSON-like formats such as MessagePack and CBOR, both BJData and UBJSON demonstrate a rare @@ -15,11 +15,30 @@ simple processing. !!! abstract "References" - - [BJData Specification](https://neurojson.org/bjdata/draft2) + - [BJData Specification (Draft 3)](https://neurojson.org/bjdata/draft3) + - [BJData Specification (Draft 2)](https://neurojson.org/bjdata/draft2) + +!!! info "Supported drafts" + + The library supports BJData Draft 2 (the default) and Draft 3, except for column-major N-dimensional arrays (see + below). The draft used for writing is selected with the + [`version` parameter](../../api/basic_json/bjdata_version_t.md) of + [`to_bjdata`](../../api/basic_json/to_bjdata.md); [`from_bjdata`](../../api/basic_json/from_bjdata.md) has no such + parameter and reads the markers of both drafts. + +!!! warning "Draft 4 and column-major ND-arrays are not supported" + + BJData Draft 4 (April 2026) adds structure-of-arrays (SoA) containers (`[$` or `{$` followed by a schema object) + and the extension type `E`. `from_bjdata` does not implement them and throws + [`parse_error.112`](../../home/exceptions.md#jsonexceptionparse_error112) for such input; `to_bjdata` never writes + them. + + Likewise, column-major ND-arrays of Draft 3 (the `#` marker followed by an array that wraps the dimension vector) + are rejected with [`parse_error.113`](../../home/exceptions.md#jsonexceptionparse_error113). ## Byte layout -BJData follows the structure of UBJSON: a one-byte, human-readable ASCII *type marker* followed by the payload of that type, with strings prefixed by a length that is itself an integer value. In contrast to UBJSON, numbers are stored in little-endian byte order, and additional markers cover unsigned 16/32/64-bit integers, half-precision floats, and bytes. Containers can be written unoptimized (opened with `[` or `{`, closed with `]` or `}`) or optimized with a declared element type (`$`) and/or count (`#`). BJData extends the optimized form: the count can be a list of dimensions, which stores an N-dimensional packed array as a flat sequence of values without any per-element markers. +BJData follows the structure of UBJSON: a one-byte, human-readable ASCII *type marker* followed by the payload of that type, with strings prefixed by a length that is itself an integer value. In contrast to UBJSON, numbers are stored in little-endian byte order, and additional markers cover unsigned 16/32/64-bit integers, half-precision floats, and bytes. Containers can be written unoptimized (opened with `[` or `{`, closed with `]` or `}`) or optimized with a count (`#`), optionally preceded by a declared element type (`$`). BJData extends the optimized form: the count can be a list of dimensions, which stores an N-dimensional packed array as a flat sequence of values without any per-element markers. Consider the following JSON value (shown formatted for readability): @@ -54,35 +73,35 @@ Consider the following JSON value (shown formatted for readability): The library uses the following mapping from JSON values types to BJData types according to the BJData specification: -| JSON value type | value/range | BJData type | marker | -|-----------------|-------------------------------------------|----------------|--------| -| null | `null` | null | `Z` | -| boolean | `true` | true | `T` | -| boolean | `false` | false | `F` | -| number_integer | -9223372036854775808..-2147483649 | int64 | `L` | -| number_integer | -2147483648..-32769 | int32 | `l` | -| number_integer | -32768..-129 | int16 | `I` | -| number_integer | -128..127 | int8 | `i` | -| number_integer | 128..255 | uint8 | `U` | -| number_integer | 256..32767 | int16 | `I` | -| number_integer | 32768..65535 | uint16 | `u` | -| number_integer | 65536..2147483647 | int32 | `l` | -| number_integer | 2147483648..4294967295 | uint32 | `m` | -| number_integer | 4294967296..9223372036854775807 | int64 | `L` | -| number_integer | 9223372036854775808..18446744073709551615 | uint64 | `M` | -| number_unsigned | 0..127 | int8 | `i` | -| number_unsigned | 128..255 | uint8 | `U` | -| number_unsigned | 256..32767 | int16 | `I` | -| number_unsigned | 32768..65535 | uint16 | `u` | -| number_unsigned | 65536..2147483647 | int32 | `l` | -| number_unsigned | 2147483648..4294967295 | uint32 | `m` | -| number_unsigned | 4294967296..9223372036854775807 | int64 | `L` | -| number_unsigned | 9223372036854775808..18446744073709551615 | uint64 | `M` | -| number_float | *any value* | float64 | `D` | -| string | *with shortest length indicator* | string | `S` | -| array | *see notes on optimized format/ND-array* | array | `[` | -| object | *see notes on optimized format* | map | `{` | -| binary | *see notes on binary values* | array | `[$B` | +| JSON value type | value/range | BJData type | marker | +|-----------------|-------------------------------------------|-------------|-------------------------------------------------------------------------| +| null | `null` | null | `Z` | +| boolean | `true` | true | `T` | +| boolean | `false` | false | `F` | +| number_integer | -9223372036854775808..-2147483649 | int64 | `L` | +| number_integer | -2147483648..-32769 | int32 | `l` | +| number_integer | -32768..-129 | int16 | `I` | +| number_integer | -128..127 | int8 | `i` | +| number_integer | 128..255 | uint8 | `U` | +| number_integer | 256..32767 | int16 | `I` | +| number_integer | 32768..65535 | uint16 | `u` | +| number_integer | 65536..2147483647 | int32 | `l` | +| number_integer | 2147483648..4294967295 | uint32 | `m` | +| number_integer | 4294967296..9223372036854775807 | int64 | `L` | +| number_integer | 9223372036854775808..18446744073709551615 | uint64 | `M` | +| number_unsigned | 0..127 | int8 | `i` | +| number_unsigned | 128..255 | uint8 | `U` | +| number_unsigned | 256..32767 | int16 | `I` | +| number_unsigned | 32768..65535 | uint16 | `u` | +| number_unsigned | 65536..2147483647 | int32 | `l` | +| number_unsigned | 2147483648..4294967295 | uint32 | `m` | +| number_unsigned | 4294967296..9223372036854775807 | int64 | `L` | +| number_unsigned | 9223372036854775808..18446744073709551615 | uint64 | `M` | +| number_float | *any value* | float64 | `D` | +| string | *with shortest length indicator* | string | `S` | +| array | *see notes on optimized format/ND-array* | array | `[` | +| object | *see notes on optimized format* | map | `{` | +| binary | *see notes on binary values* | array | `[$B` (Draft 3 with `use_size` and `use_type`), `[U`, or `[B` otherwise | !!! success "Complete mapping" @@ -107,10 +126,16 @@ The library uses the following mapping from JSON values types to BJData types ac !!! info "Unused BJData markers" - The following markers are not used in the conversion: + The following markers are not written: - - `Z`: no-op values are not created. - - `C`: single-byte strings are serialized with `S` markers. + - `N`: no-op values are not created (they are skipped when reading). + - `C`: single-character strings are serialized with `S` markers (`C` is read, and written for the elements of + ND-arrays with `"_ArrayType_": "char"`). + - `h`: floating-point numbers are written as `D`, not as float16 (`h` is read). + - `B`: only written in Draft 3 mode. + - `d` is only written for `single` ND-arrays or if `number_float_t` is `float`; `H` is only written for integers + wider than 64 bits (custom `number_integer_t`/`number_unsigned_t`). An unsigned integer above 9223372036854775807 + is written as `M`. !!! info "NaN/infinity handling" @@ -142,7 +167,7 @@ The library uses the following mapping from JSON values types to BJData types ac BJData extends UBJSON's optimized array **size** marker to support ND-arrays of uniform numerical data types (referred to as *packed arrays*). For example, the 2-D `uint8` integer array `[[1,2],[3,4],[5,6]]`, stored as nested optimized array in UBJSON `[ [$U#i2 1 2 [$U#i2 3 4 [$U#i2 5 6 ]`, can be further compressed in BJData to - `[$U#[$i#i2 2 3 1 2 3 4 5 6` or `[$U#[i2 i3] 1 2 3 4 5 6`. + `[$U#[$i#i2 3 2 1 2 3 4 5 6` or `[$U#[i3 i2] 1 2 3 4 5 6`. To maintain type and size information, ND-arrays are converted to JSON objects following the **annotated array format** (defined in the [JData specification (Draft 3)][JDataAAFmt]), when parsed using @@ -152,11 +177,14 @@ The library uses the following mapping from JSON values types to BJData types ac ```json { "_ArrayType_": "uint8", - "_ArraySize_": [2,3], + "_ArraySize_": [3,2], "_ArrayData_": [1,2,3,4,5,6] } ``` + ND-arrays with the element type `h` (float16) are returned with `"_ArrayType_": "half"`. `to_bjdata` does not pack + `"half"` objects; they are written as regular objects, which read back as an equal value. + Likewise, when a JSON object in the above form is serialized using [`to_bjdata`](../../api/basic_json/to_bjdata.md), it is automatically converted into a compact BJData ND-array. @@ -167,34 +195,40 @@ The library uses the following mapping from JSON values types to BJData types ac object; otherwise it is serialized as a regular JSON object, so the annotation is never lost in a round trip. This requires all of the following: + - the object has exactly the three keys `"_ArrayType_"`, `"_ArraySize_"`, and `"_ArrayData_"` (any other key, e.g., + `"_ArrayOrder_"`, makes it a regular object), - `"_ArrayType_"` is one of `uint8`, `int8`, `uint16`, `int16`, `uint32`, `int32`, `uint64`, `int64`, `single`, - `double`, `char`, or `byte`, + `double`, `char`, or (only with `version = bjdata_version_t::draft3`, since the `B` marker is not part of Draft 2) + `byte`; the names are case-sensitive, - `"_ArraySize_"` is an array, since the dimensions are written as the ND-array header's length, - - `"_ArraySize_"` has at least two entries and is not a 1×N row vector (first entry 1), since other shapes are - parsed back as a regular array, + - `"_ArraySize_"` has at least two entries and is not a two-entry row vector (`[1,N]`), since other shapes are + parsed back as a regular array (`[1,2,2]` is packed, `[1,4]` is not), - every entry of `"_ArraySize_"` is a positive integer, and their product is representable as a `std::size_t`, - `"_ArrayData_"` is an array holding exactly that many elements, and - - every element of `"_ArrayData_"` is a number of the kind named by `"_ArrayType_"`: for the integer types, a - value that fits the named width; for `double`, any value; for `single`, a value that survives narrowing to - `float` and back without change (for instance, `0.1` does not, since it is not exactly representable as - `float`). + - every element of `"_ArrayData_"` is a number of the kind named by `"_ArrayType_"`: for the integer types, an + integer (a floating-point number such as `1.0` does not qualify) that fits the named width (for `char`, 0..127); + for `double`, a floating-point number (an integer such as `1` does not qualify); for `single`, a floating-point + number that survives narrowing to `float` and back without change (for instance, `0.1` does not, since it is not + exactly representable as `float`). - An annotated object is always read back with its keys in the order shown above, `"_ArrayType_"`, `"_ArraySize_"`, - `"_ArrayData_"`, regardless of the order the ND-array's header stores them in on the wire. This matters for - `ordered_json`, whose comparison takes key order into account. + With `ordered_json`, an annotated object read from BJData has its keys in the order `"_ArrayType_"`, + `"_ArraySize_"`, `"_ArrayData_"`; with `json`, they are sorted like those of any other object. The current version of this library does not yet support automatic detection of and conversion from a nested JSON array input to a BJData ND-array. - [JDataAAFmt]: https://github.com/NeuroJSON/jdata/blob/master/JData_specification.md#annotated-storage-of-n-d-arrays + [JDataAAFmt]: https://github.com/NeuroJSON/jdata/blob/Draft-3/JData_specification.md#annotated-storage-of-n-d-arrays !!! info "Restrictions in optimized data types for arrays and objects" - Due to diminished space saving, hampered readability, and increased security risks, in BJData, the allowed data - types following the `$` marker in an optimized array and object container are restricted to + Due to diminished space saving, hampered readability, and increased security risks, in BJData, the + allowed data types following the `$` marker in an optimized array and object container are restricted to **non-zero-fixed-length** data types. Therefore, the valid optimized type markers can only be one of - `UiuImlMLhdDCB`. This also means other variable (`[{SH`) or zero-length types (`TFN`) can not be used in an - optimized array or object in BJData. + `UiuImlMLhdDC`, plus `B` since Draft 3 (the reader accepts `B` in both drafts). This also means other variable (`[{SH`) or zero-length types (`TFZN`) can not be used in an + optimized array or object in BJData. The library enforces this restriction for both drafts. A `C` (char) element above 127 is rejected on read with + [`parse_error.113`](../../home/exceptions.md#jsonexceptionparse_error113), in 1-D and in N-dimensional arrays. With `use_type`, the + elements must have the same marker (`[1,200,3]` is not optimized, since it mixes `i` and `U`); booleans, null, + strings, arrays, and objects are never optimized with `$`. !!! info "Binary values" @@ -202,14 +236,16 @@ The library uses the following mapping from JSON values types to BJData types ac in optimized arrays to designate binary data. This means that, unlike UBJSON, binary data can be both serialized and deserialized. - To preserve compatibility with BJData Draft 2, the Draft 3 optimized binary array must be explicitly enabled using - the `version` parameter of [`to_bjdata`](../../api/basic_json/to_bjdata.md). + To preserve compatibility with BJData Draft 2, the Draft 3 optimized binary array must be explicitly enabled: pass + `version = bjdata_version_t::draft3` together with `use_size = true` and `use_type = true` to + [`to_bjdata`](../../api/basic_json/to_bjdata.md). With Draft 3 and only `use_size` (or neither), a binary value is + written as an array of `B` elements. - In Draft2 mode (default), if the JSON data contains the binary type, the value stored as a list of integers, as + In Draft 2 mode (default), if the JSON data contains the binary type, the value is stored as a list of integers, as suggested by the BJData documentation. In particular, this means that the serialization and the deserialization of JSON containing binary values into BJData and back will result in a different JSON object. - [BJDataBinArr]: https://github.com/NeuroJSON/bjdata/blob/master/Binary_JData_Specification.md#optimized-binary-array + [BJDataBinArr]: https://github.com/NeuroJSON/bjdata/blob/Draft-3/Binary_JData_Specification.md#optimized-binary-array ??? example "Example: serialize JSON values to BJData, with and without size/type optimization" @@ -227,34 +263,46 @@ The library uses the following mapping from JSON values types to BJData types ac The library maps BJData types to JSON value types as follows: -| BJData type | JSON value type | marker | -|-------------|------------------------------------------|----------| -| no-op | *no value, next value is read* | `N` | -| null | `null` | `Z` | -| false | `false` | `F` | -| true | `true` | `T` | -| float16 | number_float | `h` | -| float32 | number_float | `d` | -| float64 | number_float | `D` | -| uint8 | number_unsigned | `U` | -| int8 | number_integer | `i` | -| uint16 | number_unsigned | `u` | -| int16 | number_integer | `I` | -| uint32 | number_unsigned | `m` | -| int32 | number_integer | `l` | -| uint64 | number_unsigned | `M` | -| int64 | number_integer | `L` | -| byte | number_unsigned | `B` | -| string | string | `S` | -| char | string | `C` | -| array | array (optimized values are supported) | `[` | -| ND-array | object (in JData annotated array format) | `[$.#[.` | -| object | object (optimized values are supported) | `{` | -| binary | binary (strongly-typed byte array) | `[$B` | +| BJData type | JSON value type | marker | +|-----------------------|--------------------------------------------------|----------| +| no-op | *no value, next value is read* | `N` | +| null | `null` | `Z` | +| false | `false` | `F` | +| true | `true` | `T` | +| float16 | number_float | `h` | +| float32 | number_float | `d` | +| float64 | number_float | `D` | +| high-precision number | number_integer, number_unsigned, or number_float | `H` | +| uint8 | number_unsigned | `U` | +| int8 | number_integer | `i` | +| uint16 | number_unsigned | `u` | +| int16 | number_integer | `I` | +| uint32 | number_unsigned | `m` | +| int32 | number_integer | `l` | +| uint64 | number_unsigned | `M` | +| int64 | number_integer | `L` | +| byte | number_unsigned | `B` | +| string | string | `S` | +| char | string | `C` | +| array | array (optimized values are supported) | `[` | +| ND-array | object (in JData annotated array format) | `[$.#[.` | +| object | object (optimized values are supported) | `{` | +| binary | binary (strongly-typed byte array) | `[$B` | !!! success "Complete mapping" - The mapping is **complete** in the sense that any BJData value can be converted to a JSON value. + The mapping is **complete** for the BJData types of Draft 2 and Draft 3 with one exception: column-major + N-dimensional arrays are rejected with [`parse_error.113`](../../home/exceptions.md#jsonexceptionparse_error113). + The Draft 4 constructs (structure-of-arrays containers and the extension type `E`) are not supported either. + + The digits of an `H` value are parsed like a JSON number: integers that fit 64 bits become `number_integer` or + `number_unsigned`, anything else a `number_float`; invalid text throws + [`parse_error.115`](../../home/exceptions.md#jsonexceptionparse_error115). + +!!! info "Duplicate object keys" + + If an object contains the same key more than once, `from_bjdata` accepts it and keeps the last value; with + `ordered_json`, the key keeps the position of its first occurrence. !!! warning "Ill-formed UTF-8 in string values and object keys" diff --git a/docs/mkdocs/docs/features/binary_formats/index.md b/docs/mkdocs/docs/features/binary_formats/index.md index 9c55a9a52..6e1ada110 100644 --- a/docs/mkdocs/docs/features/binary_formats/index.md +++ b/docs/mkdocs/docs/features/binary_formats/index.md @@ -16,25 +16,25 @@ to efficiently encode JSON values to byte vectors and to decode such vectors. ### Completeness -| Format | Serialization | Deserialization | -|-------------|-----------------------------------------------|----------------------------------------------| -| BJData | complete | complete | -| BON8 | incomplete: no unsigned integers above int64 | complete | -| BSON | incomplete: top-level value must be an object | incomplete, but all JSON types are supported | -| CBOR | complete | incomplete, but all JSON types are supported | -| MessagePack | complete | incomplete, but all JSON types are supported | -| UBJSON | complete | complete | +| Format | Serialization | Deserialization | +|-------------|-----------------------------------------------|-----------------------------------------------------------------------| +| BJData | complete | incomplete: no column-major ND-arrays, no Draft 4 SoA/extension types | +| BON8 | incomplete: no unsigned integers above int64 | complete | +| BSON | incomplete: top-level value must be an object | incomplete, but all JSON types are supported | +| CBOR | complete | incomplete, but all JSON types are supported | +| MessagePack | complete | incomplete, but all JSON types are supported | +| UBJSON | complete | complete | ### Binary values -| Format | Binary values | Binary subtypes | -|-------------|---------------|-----------------| -| BJData | not supported | not supported | -| BON8 | not supported | not supported | -| BSON | supported | supported | -| CBOR | supported | supported | -| MessagePack | supported | supported | -| UBJSON | not supported | not supported | +| Format | Binary values | Binary subtypes | +|-------------|--------------------------|-----------------| +| BJData | supported (Draft 3 only) | not supported | +| BON8 | not supported | not supported | +| BSON | supported | supported | +| CBOR | supported | supported | +| MessagePack | supported | supported | +| UBJSON | not supported | not supported | See [binary values](../binary_values.md) for more information. diff --git a/docs/mkdocs/docs/features/binary_values.md b/docs/mkdocs/docs/features/binary_values.md index e4dee45d4..f0e10247a 100644 --- a/docs/mkdocs/docs/features/binary_values.md +++ b/docs/mkdocs/docs/features/binary_values.md @@ -132,8 +132,11 @@ is an integer or `null`. ### BJData -[BJData](binary_formats/bjdata.md) neither supports binary values nor subtypes and proposes to serialize binary values -as an array of uint8 values. The library implements this translation. +[BJData](binary_formats/bjdata.md) Draft 3 supports binary values as optimized arrays of `B` (byte) elements, but no +subtypes. With `version = bjdata_version_t::draft3` and both `use_size` and `use_type` set, `to_bjdata` writes a binary +value as `[$B#...`, and `from_bjdata` reads it back as a binary value (without subtype). In all other cases, including +the default Draft 2 mode, the library follows the Draft 2 proposal and writes an array of integers (`U` in Draft 2, `B` +in Draft 3), which is read back as an array. ??? example "Example: serialize a binary value to BJData" @@ -171,7 +174,7 @@ as an array of uint8 values. The library implements this translation. ```c 0x7B // '{' - 0x23 0x69 0x01 // '#' 'i' type of the array elements: unsigned integers + 0x23 0x69 0x01 // '#' 'i' 1 number of object members 0x69 0x06 // i 6 (length of the key) 0x62 0x69 0x6E 0x61 0x72 0x79 // "binary" 0x5B // '[' array @@ -180,7 +183,7 @@ as an array of uint8 values. The library implements this translation. 0xCA 0xFE 0xBA 0xBE // content ``` - Note that subtype (42) is **not** serialized and that BJData has **no binary type**, and deserializing `v` would + Note that subtype (42) is **not** serialized and that, in Draft 2 mode, BJData has **no binary type**, and deserializing `v` would yield the following value: ```json diff --git a/docs/mkdocs/docs/home/exceptions.md b/docs/mkdocs/docs/home/exceptions.md index 48825cb25..93dbd0997 100644 --- a/docs/mkdocs/docs/home/exceptions.md +++ b/docs/mkdocs/docs/home/exceptions.md @@ -418,13 +418,16 @@ The parsing of the corresponding BSON record type is not implemented (yet). ### json.exception.parse_error.115 -A UBJSON high-precision number could not be parsed. +A UBJSON or BJData high-precision number could not be parsed. -!!! failure "Example message" +!!! failure "Example messages" ``` [json.exception.parse_error.115] parse error at byte 5: syntax error while parsing UBJSON high-precision number: invalid number text: 1A ``` + ``` + [json.exception.parse_error.115] parse error at byte 5: syntax error while parsing BJData high-precision number: invalid number text: 1A + ``` ### json.exception.parse_error.116 @@ -1025,7 +1028,7 @@ exception, because they serialize integers beyond 64 bits as high-precision numb - Since version 3.12.0, integer numbers beyond int64 are serialized as uint64 BSON numbers. Before version 3.13.0, CBOR, MessagePack, and BSON silently truncated integers wider than 64 bits, and BJData - truncated unsigned integers wider than 64 bits. + truncated unsigned integers wider than 64 bits that were elements of ND-arrays with `"_ArrayType_": "uint64"`. !!! failure "Example messages" @@ -1240,8 +1243,9 @@ A JSON Patch operation 'test' failed. The unsuccessful operation is also printed ### json.exception.other_error.502 [`to_ubjson`](../api/basic_json/to_ubjson.md) and [`to_bjdata`](../api/basic_json/to_bjdata.md) were called with -`use_type = true` but `use_size = false` for a value that contains a non-empty array, object, or binary value. UBJSON -requires a size marker (`#`) after a type marker (`$`); scalars and empty containers do not throw. +`use_type = true` but `use_size = false`, and the value contains a non-empty array or object (or, with BJData Draft 3, +any binary value). UBJSON requires a size marker (`#`) after a type marker (`$`); scalars and empty arrays or objects +do not throw. !!! failure "Example message" diff --git a/docs/mkdocs/mkdocs.yml b/docs/mkdocs/mkdocs.yml index 7e1c23aa8..f2b99cf3a 100644 --- a/docs/mkdocs/mkdocs.yml +++ b/docs/mkdocs/mkdocs.yml @@ -173,6 +173,7 @@ nav: - 'begin': api/basic_json/begin.md - 'binary': api/basic_json/binary.md - 'binary_t': api/basic_json/binary_t.md + - 'bjdata_version_t': api/basic_json/bjdata_version_t.md - 'boolean_t': api/basic_json/boolean_t.md - 'cbegin': api/basic_json/cbegin.md - 'cbor_tag_handler_t': api/basic_json/cbor_tag_handler_t.md diff --git a/include/nlohmann/detail/input/binary_reader.hpp b/include/nlohmann/detail/input/binary_reader.hpp index b44fd868c..9ed0e4d1b 100644 --- a/include/nlohmann/detail/input/binary_reader.hpp +++ b/include/nlohmann/detail/input/binary_reader.hpp @@ -3277,6 +3277,8 @@ class binary_reader // get_ubjson_size_value() (the type marker is known before the dimension vector // that determines size_and_type.first is read, so it is emitted first there to // match the documented _ArrayType_, _ArraySize_, _ArrayData_ key order) + // 'C' elements are read as unsigned bytes, but BJData limits a char to 0..127 + const bool nd_is_char = size_and_type.second == 'C'; if (size_and_type.second == 'C' || size_and_type.second == 'B') { size_and_type.second = 'U'; @@ -3290,6 +3292,24 @@ class binary_reader for (std::size_t i = 0; i < size_and_type.first; ++i) { + if (nd_is_char) + { + get(); + if (JSON_HEDLEY_UNLIKELY(!unexpect_eof("char"))) + { + return false; + } + if (JSON_HEDLEY_UNLIKELY(current > 127)) + { + return last_byte_error(exception_id::invalid_string_or_size, "byte after 'C' must be in range 0x00..0x7F", "char", ""); + } + if (JSON_HEDLEY_UNLIKELY(!sax->number_unsigned(static_cast(current)))) + { + return false; + } + continue; + } + if (JSON_HEDLEY_UNLIKELY(!get_ubjson_value(size_and_type.second))) { return false; @@ -4655,6 +4675,8 @@ class binary_reader return "uint8"; case 'd': return "single"; + case 'h': + return "half"; case 'i': return "int8"; case 'l': diff --git a/include/nlohmann/detail/output/binary_writer.hpp b/include/nlohmann/detail/output/binary_writer.hpp index d8b3e7162..8732e52b4 100644 --- a/include/nlohmann/detail/output/binary_writer.hpp +++ b/include/nlohmann/detail/output/binary_writer.hpp @@ -3848,7 +3848,7 @@ class binary_writer A C++11 `constexpr` function cannot contain a `switch`, so this is a plain comparison chain instead; it is only reached once per ND-array candidate object. Keep in sync with binary_reader's `bjd_type_name()`, which maps - the other way. + the other way (it also reads "half", which is never written). */ static CharType bjdata_ndarray_type_marker(const string_t& name) { @@ -4056,10 +4056,13 @@ class binary_writer switch (dtype) { case 'U': - case 'C': case 'B': ok = write_bjdata_ndarray_element(el, dry_run); break; + case 'C': + // BJData limits a char to 0..127 (the intersection of the int8 and uint8 ranges) + ok = bjdata_ndarray_value_in_range(el) && bjdata_ndarray_value_in_range(el); + break; case 'i': ok = write_bjdata_ndarray_element(el, dry_run); break; diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index b67223f0c..eca217237 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -20744,6 +20744,8 @@ class binary_reader // get_ubjson_size_value() (the type marker is known before the dimension vector // that determines size_and_type.first is read, so it is emitted first there to // match the documented _ArrayType_, _ArraySize_, _ArrayData_ key order) + // 'C' elements are read as unsigned bytes, but BJData limits a char to 0..127 + const bool nd_is_char = size_and_type.second == 'C'; if (size_and_type.second == 'C' || size_and_type.second == 'B') { size_and_type.second = 'U'; @@ -20757,6 +20759,24 @@ class binary_reader for (std::size_t i = 0; i < size_and_type.first; ++i) { + if (nd_is_char) + { + get(); + if (JSON_HEDLEY_UNLIKELY(!unexpect_eof("char"))) + { + return false; + } + if (JSON_HEDLEY_UNLIKELY(current > 127)) + { + return last_byte_error(exception_id::invalid_string_or_size, "byte after 'C' must be in range 0x00..0x7F", "char", ""); + } + if (JSON_HEDLEY_UNLIKELY(!sax->number_unsigned(static_cast(current)))) + { + return false; + } + continue; + } + if (JSON_HEDLEY_UNLIKELY(!get_ubjson_value(size_and_type.second))) { return false; @@ -22122,6 +22142,8 @@ class binary_reader return "uint8"; case 'd': return "single"; + case 'h': + return "half"; case 'i': return "int8"; case 'l': @@ -30280,7 +30302,7 @@ class binary_writer A C++11 `constexpr` function cannot contain a `switch`, so this is a plain comparison chain instead; it is only reached once per ND-array candidate object. Keep in sync with binary_reader's `bjd_type_name()`, which maps - the other way. + the other way (it also reads "half", which is never written). */ static CharType bjdata_ndarray_type_marker(const string_t& name) { @@ -30488,10 +30510,13 @@ class binary_writer switch (dtype) { case 'U': - case 'C': case 'B': ok = write_bjdata_ndarray_element(el, dry_run); break; + case 'C': + // BJData limits a char to 0..127 (the intersection of the int8 and uint8 ranges) + ok = bjdata_ndarray_value_in_range(el) && bjdata_ndarray_value_in_range(el); + break; case 'i': ok = write_bjdata_ndarray_element(el, dry_run); break; diff --git a/tests/src/unit-bjdata.cpp b/tests/src/unit-bjdata.cpp index 4a34125ba..e4dffb6d0 100644 --- a/tests/src/unit-bjdata.cpp +++ b/tests/src/unit-bjdata.cpp @@ -2916,6 +2916,35 @@ TEST_CASE("BJData") CHECK(json::from_bjdata(out_single_ok) == json({{"_ArrayType_", "single"}, {"_ArraySize_", {2, 1}}, {"_ArrayData_", {1.5f, -1.5f}}})); } + SECTION("ndarray with _ArrayType_ \"char\" is limited to 0..127") + { + // both BJData drafts limit a char to 0..127; larger values must not + // be written as 'C' elements (the reader rejects them), so such an + // object stays a plain object that round-trips + for (const auto version : + { + json::bjdata_version_t::draft2, json::bjdata_version_t::draft3 + }) + { + json const j_ok = json({{"_ArrayType_", "char"}, {"_ArraySize_", {2, 2}}, {"_ArrayData_", {0, 97, 98, 127}}}); + const auto out_ok = json::to_bjdata(j_ok, true, true, version); + CHECK(out_ok == std::vector({'[', '$', 'C', '#', '[', '$', 'i', '#', 'i', 2, 2, 2, 0, 97, 98, 127})); + CHECK(json::from_bjdata(out_ok) == j_ok); + + for (const int bad : + { + 128, 200, 255, 256, -1 + }) + { + CAPTURE(bad) + json const j_bad = json({{"_ArrayType_", "char"}, {"_ArraySize_", {2, 2}}, {"_ArrayData_", {97, 98, 99, bad}}}); + const auto out_bad = json::to_bjdata(j_bad, true, true, version); + CHECK(out_bad.at(0) == '{'); + CHECK(json::from_bjdata(out_bad) == j_bad); + } + } + } + SECTION("ndarray annotation keys are read back in the documented order") { // from_bjdata() must emit the annotation object's keys in the order @@ -3324,6 +3353,44 @@ TEST_CASE("BJData") } } + SECTION("ndarray of half-precision floats can be read") + { + // the 'h' element type is read as _ArrayType_ "half" + json _; + const std::vector half = {'[', '$', 'h', '#', '[', '$', 'U', '#', 'U', 2, 2, 2, 0x00, 0x3C, 0x00, 0x40, 0x00, 0x42, 0x00, 0x44}; + const json j_half = json::parse(R"({"_ArrayType_":"half","_ArraySize_":[2,2],"_ArrayData_":[1.0,2.0,3.0,4.0]})"); + CHECK(json::from_bjdata(half) == j_half); + + // the writer does not pack "half": it writes a plain object that reads back equal + CHECK(json::from_bjdata(json::to_bjdata(j_half)) == j_half); + CHECK(json::from_bjdata(json::to_bjdata(j_half, true, true, json::bjdata_version_t::draft3)) == j_half); + + // truncated half data + CHECK_THROWS_AS(_ = json::from_bjdata(std::vector(half.begin(), half.end() - 1)), json::parse_error&); + } + + SECTION("ndarray of char rejects values above 127") + { + json _; + const std::vector ok = {'[', '$', 'C', '#', '[', '$', 'U', '#', 'U', 2, 2, 2, 'a', 'b', 'c', 0x7F}; + CHECK(json::from_bjdata(ok) == json::parse(R"({"_ArrayType_":"char","_ArraySize_":[2,2],"_ArrayData_":[97,98,99,127]})")); + + std::vector bad = ok; + bad.back() = 0xC8; + CHECK_THROWS_AS(_ = json::from_bjdata(bad), json::parse_error&); + CHECK(json::from_bjdata(bad, true, false).is_discarded()); + + // the same restriction holds for 1-D arrays + CHECK_THROWS_AS(_ = json::from_bjdata(std::vector({'[', '$', 'C', '#', 'i', 1, 0xC8})), json::parse_error&); + + // truncated input + CHECK_THROWS_AS(_ = json::from_bjdata(std::vector(ok.begin(), ok.end() - 1)), json::parse_error&); + + // 'B' (byte) elements may use the full range + const std::vector byte_nd = {'[', '$', 'B', '#', '[', '$', 'U', '#', 'U', 2, 2, 2, 1, 2, 3, 0xC8}; + CHECK(json::from_bjdata(byte_nd) == json::parse(R"({"_ArrayType_":"byte","_ArraySize_":[2,2],"_ArrayData_":[1,2,3,200]})")); + } + SECTION("do not accept NTFZ markers in ndarray optimized type (with count)") { json _; From 24078c862d99f0b24eac6bb0c1a245ca2e6db74f Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Mon, 26 Oct 2026 18:25:46 +0100 Subject: [PATCH 12/18] docs: fix BON8 documentation gaps found in the format review (#5932) * docs: fix BON8 documentation gaps found in the format review Verified each claim with small C++ probes before writing it. - index.md: BON8 serialization is also incomplete for invalid UTF-8. - bon8.md: document duplicate keys (last wins), the error ids for malformed input incl. 0xF5..0xF7 + 0x80..0xBF, exact string termination wording, wide integers and discarded values in the unsupported list, and title two untitled admonitions. - exceptions.md: to_bon8 in type_error.316, BON8 in parse_error.112 (with example) and .113, wide integers in out_of_range.407. - binary_values.md: bytes 0..39 take one byte in BON8. - from_bon8.md: note on strict UTF-8 and duplicate keys. Signed-off-by: Niels Lohmann * Address review of the BON8 documentation Restore the correct statement that marker bytes cannot begin a UTF-8 character (0x80..0xBF occur in valid UTF-8 as continuation bytes), and reword two sentences. Signed-off-by: Niels Lohmann * docs: drop the discarded-value remark from the BON8 page Discarded values are not mentioned on the individual format pages. Signed-off-by: Niels Lohmann * docs: do not list invalid UTF-8 strings as a BON8 serialization gap JSON text must be valid UTF-8, and to_bon8 rejects ill-formed strings by design (type_error.316), as documented on the BON8 page. Restore the original cell and realign the table. Signed-off-by: Niels Lohmann --------- Signed-off-by: Niels Lohmann --- docs/mkdocs/docs/api/basic_json/from_bon8.md | 5 +++++ .../docs/features/binary_formats/bon8.md | 22 ++++++++++++++----- docs/mkdocs/docs/features/binary_values.md | 2 +- docs/mkdocs/docs/home/exceptions.md | 18 +++++++++++---- 4 files changed, 36 insertions(+), 11 deletions(-) diff --git a/docs/mkdocs/docs/api/basic_json/from_bon8.md b/docs/mkdocs/docs/api/basic_json/from_bon8.md index e566b8688..cd0bac558 100644 --- a/docs/mkdocs/docs/api/basic_json/from_bon8.md +++ b/docs/mkdocs/docs/api/basic_json/from_bon8.md @@ -74,6 +74,11 @@ Strong guarantee: if an exception is thrown, there are no changes in the JSON va - Throws [parse_error.112](../../home/exceptions.md#jsonexceptionparse_error112) if a parse error occurs, for instance an invalid byte, a string that is not valid UTF-8, or an object key that is not a string +The UTF-8 validity of strings is always checked, independent of +[`JSON_STRICT_BINARY_UTF8`](../macros/json_strict_binary_utf8.md), because the end of a BON8 string is determined from +its encoding. Duplicate object keys are accepted; the last value wins (see +[BON8](../../features/binary_formats/bon8.md)). + ## Complexity Linear in the size of the input. diff --git a/docs/mkdocs/docs/features/binary_formats/bon8.md b/docs/mkdocs/docs/features/binary_formats/bon8.md index b972dc035..9efa00b73 100644 --- a/docs/mkdocs/docs/features/binary_formats/bon8.md +++ b/docs/mkdocs/docs/features/binary_formats/bon8.md @@ -1,6 +1,6 @@ # BON8 -BON8 (Binary Object Notation 8) is a compact binary serialization format for JSON values. It uses the byte values that +BON8 (Binary Object Notation 8) is a compact binary serialization format for JSON values. It uses byte values that cannot begin a UTF-8 character as type markers, so strings are stored as plain UTF-8 without a length prefix: a string ends at the first byte that cannot continue it. Integers from -10 to 39, `true`, `false`, `null`, and the floating-point values -1.0, 0.0, and 1.0 take a single byte, and arrays and objects with up to four elements need no terminator. @@ -12,7 +12,7 @@ values -1.0, 0.0, and 1.0 take a single byte, and arrays and objects with up to ## Byte layout -BON8 exploits the fact that some byte values can never start a UTF-8 character. These bytes serve as *type markers*, so strings can be stored as plain UTF-8 without a length prefix: a string ends at the first byte that cannot continue it (often the marker of the next value). Small integers, `true`, `false`, `null`, and a few common floats are a single byte. Arrays and objects are introduced by a marker; those with up to four elements encode their size in the marker and need no terminator, while larger ones are closed by the end marker `0xFE`. +BON8 exploits the fact that some byte values can never start a UTF-8 character. These bytes serve as *type markers*, so strings can be stored as plain UTF-8 without a length prefix: a string ends at the first byte that cannot continue it: a marker byte, or a lead byte 0xC2..0xF7 followed by a byte outside 0x80..0xBF. The ending byte is often the marker of the next value. Small integers, `true`, `false`, `null`, and a few common floats are a single byte. Arrays and objects are introduced by a marker; those with up to four elements encode their size in the marker and need no terminator, while larger ones are closed by the end marker `0xFE`. Consider the following JSON value (shown formatted for readability): @@ -33,7 +33,7 @@ Consider the following JSON value (shown formatted for readability):
- + @@ -100,7 +100,8 @@ serialized as 0x81 0x65 0xFF, but `[1,2,3,4,"e"]` as 0x85 0x91 0x92 0x93 0x94 0x The following values can **not** be converted to a BON8 value: - - unsigned integers above 9223372036854775807, because BON8 has no unsigned 64-bit integer type + - integers outside the int64 range (unsigned integers above 9223372036854775807 and, for number types wider than + 64 bits, any larger or smaller integer), because BON8 integers are limited to int64 ([out_of_range.407](../../home/exceptions.md#jsonexceptionout_of_range407)) - strings that are not valid UTF-8, because the end of a string is determined from its encoding ([type_error.316](../../home/exceptions.md#jsonexceptiontype_error316)) @@ -168,7 +169,13 @@ The library maps BON8 types to JSON value types as follows: Non-negative integers are read as number_unsigned, negative integers as number_integer. -!!! info +A byte 0xF5..0xF7 followed by 0x80..0xBF is neither a string nor an integer and is rejected with +[parse_error.112](../../home/exceptions.md#jsonexceptionparse_error112) (invalid UTF-8 byte). The same error is +reported for any other invalid byte (such as a stray 0xFE), for ill-formed UTF-8, and for an object key that is not a +string. Input that ends prematurely, trailing data after the value, and a missing final 0xFF after a string at the end +of the message are reported as [parse_error.110](../../home/exceptions.md#jsonexceptionparse_error110). + +!!! info "Non-canonical input" Values that do not use the canonical representation, such as integers with a longer encoding than necessary, arrays and objects with up to four elements that are terminated by 0xFE, unsorted object keys, or a 0xFF after a @@ -176,7 +183,10 @@ Non-negative integers are read as number_unsigned, negative integers as number_i Strings must be valid UTF-8, and a string at the very end of a message must be terminated by 0xFF. -!!! info + Duplicate object keys are accepted; the last value wins (`ordered_json` keeps the position of the first occurrence). + The specification does not say how to treat them; the HikoGUI reference decoder keeps the first value. + +!!! info "Round trips" Any BON8 output created by `to_bon8` can be successfully parsed by `from_bon8`. diff --git a/docs/mkdocs/docs/features/binary_values.md b/docs/mkdocs/docs/features/binary_values.md index f0e10247a..5be9e5ba7 100644 --- a/docs/mkdocs/docs/features/binary_values.md +++ b/docs/mkdocs/docs/features/binary_values.md @@ -216,7 +216,7 @@ array of integers. 0x87 // object with 1 member 0x62 0x69 0x6E 0x61 0x72 0x79 // "binary" 0x84 // array with 4 elements - 0xC3 0x22 0xC3 0x56 0xC3 0x12 0xC3 0x16 // content (each byte as a 2-byte integer) + 0xC3 0x22 0xC3 0x56 0xC3 0x12 0xC3 0x16 // content (each byte, here all above 39, as a 2-byte integer) ``` Note that the subtype is lost, and deserializing `v` would yield the following value: diff --git a/docs/mkdocs/docs/home/exceptions.md b/docs/mkdocs/docs/home/exceptions.md index 93dbd0997..65b31c93c 100644 --- a/docs/mkdocs/docs/home/exceptions.md +++ b/docs/mkdocs/docs/home/exceptions.md @@ -346,7 +346,7 @@ been read, or (with `strict = true`) further bytes follow the value. ### json.exception.parse_error.112 -An unexpected byte was read in a [binary format](../features/binary_formats/index.md) or length information is invalid ([BSON](../features/binary_formats/bson.md)). +An unexpected byte was read in a [binary format](../features/binary_formats/index.md) or length information is invalid ([BSON](../features/binary_formats/bson.md)). In [BON8](../features/binary_formats/bon8.md), this exception is also thrown for a key that is not a string and for ill-formed UTF-8. !!! failure "Example messages" @@ -371,15 +371,21 @@ An unexpected byte was read in a [binary format](../features/binary_formats/inde ``` [json.exception.parse_error.112] parse error at byte 5: syntax error while parsing BSON document: document size 6 does not match the number of bytes read (5) ``` + ``` + [json.exception.parse_error.112] parse error at byte 2: syntax error while parsing BON8 string: invalid UTF-8 byte: 0x80 + ``` ### json.exception.parse_error.113 A string could not be read from a [binary format](../features/binary_formats/index.md): either a value that is not a string was read where one was required (for instance as a map key), the string's length specification is invalid, or the string's bytes are not valid UTF-8 and the `error_handler` parameter of the corresponding `from_*` function is -set to `strict`. By default (`error_handler_t::keep`), the bytes of a string are not checked for valid UTF-8 on read; -see the ill-formed UTF-8 notes on the individual [binary format](../features/binary_formats/index.md) pages for how +set to `strict`. By default (`error_handler_t::keep`), the bytes of a string are not checked for valid UTF-8 on read. +This applies to the `from_*` functions that have an `error_handler` parameter; `from_bon8` does not have one. +See the ill-formed UTF-8 notes on the individual [binary format](../features/binary_formats/index.md) pages for how such a string is handled depending on `error_handler`. +[`from_bon8()`](../api/basic_json/from_bon8.md) never throws this exception: BON8 non-string keys and ill-formed UTF-8 +are reported as [parse_error.112](#jsonexceptionparse_error112). CBOR and MessagePack allow map keys of any type, but JSON object keys are always strings. Maps with keys of any other type (for instance integers or `null`) are therefore not supported; see the notes on @@ -815,6 +821,9 @@ The binary writers [`to_cbor()`](../api/basic_json/to_cbor.md), [`to_ubjson()`]( as well for a string value or object key that is not valid UTF-8 if their `error_handler` is `strict` (the default if [`JSON_STRICT_BINARY_UTF8`](../api/macros/json_strict_binary_utf8.md) is enabled). So does [`to_msgpack()`](../api/basic_json/to_msgpack.md) if `error_handler_t::strict` is passed. +[`to_bon8()`](../api/basic_json/to_bon8.md) always throws this exception for such a string or key, regardless of +[`JSON_STRICT_BINARY_UTF8`](../api/macros/json_strict_binary_utf8.md), because the end of a BON8 string is determined +from its UTF-8 encoding. !!! failure "Example message" @@ -1009,7 +1018,8 @@ double-precision number when `number_float_t` is `#!cpp float`. An integer number cannot be represented by the binary format it is serialized to: - [BON8](../features/binary_formats/bon8.md) only stores integers that fit into int64, so - [`to_bon8`](../api/basic_json/to_bon8.md) throws for unsigned integers greater than 9223372036854775807. + [`to_bon8`](../api/basic_json/to_bon8.md) throws for unsigned integers greater than 9223372036854775807 and, with a + number type wider than 64 bits, for any integer outside the int64 range. - [CBOR](../features/binary_formats/cbor.md), [MessagePack](../features/binary_formats/messagepack.md), and [BSON](../features/binary_formats/bson.md) store integers in at most 64 bits. With the default number types, every integer fits, but a [`number_integer_t`](../api/basic_json/number_integer_t.md) or From 731f1b5bfb06527fcf10b4f62291ed363472974e Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Mon, 26 Oct 2026 20:13:30 +0100 Subject: [PATCH 13/18] json_view: view_mode enum instead of bool Editable, node_kind for node::kind (#5919) * json_view: view_mode enum instead of bool Editable, node_kind for node::kind basic_json_document and basic_json_view take a view_mode (read_only, the default, or editable) instead of a bool: basic_json_document did not say what true means. The shortcut aliases are unchanged. The kind of a node is a node_kind (the value_t values plus link, 10 as before), which removes the static_cast(value_t::...) of every comparison. The node and image layouts are unchanged. Signed-off-by: Niels Lohmann * json_view: node_kind in the node writers, spell out read_only, comment fixes The builder and the edit functions take and store a node_kind instead of casting a value_t; the numbering assertions and the comments now speak of node_kind, and the link kind notes that it follows value_t::discarded. The read-only aliases name view_mode::read_only like the editable ones. view_mode.md says the mode is a compile-time property of the type. Signed-off-by: Niels Lohmann * json_view: views of read-only documents convert to editable views A read-only document has no edits, so a view of it reads correctly as a view of an editable document before its first edit. The implicit conversion lets a function taking a json_editable_view accept the views of both modes. There is no conversion the other way: a read-only view of an editable document would read wrong values after the next edit. The mode comparisons in the cross-mode operator==/!= use an is_other_mode trait, which astyle keeps compact. Signed-off-by: Niels Lohmann --------- Signed-off-by: Niels Lohmann --- BUILD.bazel | 1 + .../docs/api/basic_json_document/erase.md | 2 +- .../docs/api/basic_json_document/index.md | 25 +- .../docs/api/basic_json_document/insert.md | 2 +- .../docs/api/basic_json_document/push_back.md | 2 +- .../docs/api/basic_json_document/save.md | 2 +- .../docs/api/basic_json_document/set.md | 2 +- .../api/basic_json_view/basic_json_view.md | 21 +- docs/mkdocs/docs/api/basic_json_view/index.md | 11 +- docs/mkdocs/docs/api/json_document.md | 2 +- .../mkdocs/docs/api/json_editable_document.md | 2 +- docs/mkdocs/docs/api/json_editable_view.md | 2 +- docs/mkdocs/docs/api/json_view.md | 2 +- docs/mkdocs/docs/api/ordered_json_document.md | 2 +- .../api/ordered_json_editable_document.md | 2 +- .../docs/api/ordered_json_editable_view.md | 2 +- docs/mkdocs/docs/api/ordered_json_view.md | 2 +- docs/mkdocs/docs/api/view_mode.md | 88 +++++ .../ordered_json_editable_document.cpp | 2 +- docs/mkdocs/docs/examples/view_mode.cpp | 36 ++ docs/mkdocs/docs/examples/view_mode.output | 3 + docs/mkdocs/docs/features/json_view.md | 4 +- docs/mkdocs/mkdocs.yml | 1 + include/nlohmann/detail/view/builder.hpp | 38 +-- .../nlohmann/detail/view/document_data.hpp | 2 +- include/nlohmann/detail/view/edit.hpp | 86 +++-- include/nlohmann/detail/view/edit_storage.hpp | 6 +- include/nlohmann/detail/view/image.hpp | 20 +- include/nlohmann/detail/view/lookup.hpp | 2 +- include/nlohmann/detail/view/materialize.hpp | 2 +- include/nlohmann/detail/view/node.hpp | 41 ++- include/nlohmann/detail/view/serializer.hpp | 2 +- include/nlohmann/detail/view/view_mode.hpp | 52 +++ include/nlohmann/json_view.hpp | 74 ++-- single_include/nlohmann/json_view.hpp | 323 +++++++++++------- tests/src/unit-json_view_builder.cpp | 4 +- tests/src/unit-json_view_edit_values.cpp | 109 +++++- tests/src/unit-json_view_image.cpp | 18 +- 38 files changed, 704 insertions(+), 293 deletions(-) create mode 100644 docs/mkdocs/docs/api/view_mode.md create mode 100644 docs/mkdocs/docs/examples/view_mode.cpp create mode 100644 docs/mkdocs/docs/examples/view_mode.output create mode 100644 include/nlohmann/detail/view/view_mode.hpp diff --git a/BUILD.bazel b/BUILD.bazel index b6b5152a2..2b1c9f146 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -90,6 +90,7 @@ cc_library( "include/nlohmann/detail/view/simd.hpp", "include/nlohmann/detail/view/string_ref.hpp", "include/nlohmann/detail/view/value.hpp", + "include/nlohmann/detail/view/view_mode.hpp", "include/nlohmann/json.hpp", "include/nlohmann/json_fwd.hpp", "include/nlohmann/json_literals.hpp", diff --git a/docs/mkdocs/docs/api/basic_json_document/erase.md b/docs/mkdocs/docs/api/basic_json_document/erase.md index 4cbc50d19..7d453addd 100644 --- a/docs/mkdocs/docs/api/basic_json_document/erase.md +++ b/docs/mkdocs/docs/api/basic_json_document/erase.md @@ -12,7 +12,7 @@ void erase(view_type array, I idx); std::size_t erase(const json_pointer& ptr); ``` -Only an **editable** document (`#!cpp Editable == true`, e.g. [`json_editable_document`](../json_editable_document.md)) +Only an **editable** document (`#!cpp view_mode::editable`, e.g. [`json_editable_document`](../json_editable_document.md)) has `erase`; calling it on a read-only `basic_json_document` fails to compile (`#!cpp static_assert`). 1. Removes every member of `object` whose key is `key` (see [Notes](#notes) on duplicate keys) and returns how many diff --git a/docs/mkdocs/docs/api/basic_json_document/index.md b/docs/mkdocs/docs/api/basic_json_document/index.md index ea5f449db..b0743e4ec 100644 --- a/docs/mkdocs/docs/api/basic_json_document/index.md +++ b/docs/mkdocs/docs/api/basic_json_document/index.md @@ -3,7 +3,7 @@ Defined in header `` ```cpp -template +template class basic_json_document; ``` @@ -20,9 +20,9 @@ is move-only: a copy would duplicate the whole index, the decoded-string buffer, enough in every use we know of. The index, the decoded strings, and the owned text live in one heap block that the document holds through a `#!cpp std::unique_ptr`; this is also why views survive a move of the document. -With `#!cpp Editable == true`, the document also offers [`set`](set.md), [`push_back`](push_back.md), +With `#!cpp Mode == view_mode::editable`, the document also offers [`set`](set.md), [`push_back`](push_back.md), [`insert`](insert.md), and [`erase`](erase.md) to change values in place, see [Edits](#edits) below. The source text -itself is never written; a read-only document (`#!cpp Editable == false`, the default) does not carry any of the +itself is never written; a read-only document (`#!cpp view_mode::read_only`, the default) does not carry any of the bookkeeping edits need, and calling any of them on one fails to compile (`#!cpp static_assert`). ## Template parameters @@ -32,9 +32,10 @@ bookkeeping edits need, and calling any of them on one fails to compile (`#!cpp [`ordered_json`](../ordered_json.md). Only 64-bit `number_integer_t`/`number_unsigned_t` types are supported; this is checked with a `static_assert`. -`Editable` -: whether the document supports [`set`](set.md), [`push_back`](push_back.md), [`insert`](insert.md), and - [`erase`](erase.md) (optional, `#!cpp false` by default). See [Edits](#edits) below. +`Mode` +: a [`view_mode`](../view_mode.md): `#!cpp view_mode::editable` if the document supports [`set`](set.md), + [`push_back`](push_back.md), [`insert`](insert.md), and [`erase`](erase.md) (optional, `#!cpp view_mode::read_only` + by default). See [Edits](#edits) below. ## Specializations @@ -47,7 +48,7 @@ bookkeeping edits need, and calling any of them on one fails to compile (`#!cpp ## Member types -- **view_type** - the type of view returned by [`root()`](root.md) (`#!cpp basic_json_view`) +- **view_type** - the type of view returned by [`root()`](root.md) (`#!cpp basic_json_view`) - **value_t** - the JSON type enumeration, see [`basic_json::value_t`](../basic_json/value_t.md) - **image_check** - how [`load()`](load.md) validates an image (`full`, `bounds`, `none`), see [`image_check`](load.md#image_check) @@ -80,15 +81,15 @@ bookkeeping edits need, and calling any of them on one fails to compile (`#!cpp ### Edits - [**set**](set.md) - replace a value, or set an object member, an array element, or the value a JSON pointer refers - to (`#!cpp Editable` documents only) -- [**push_back**](push_back.md) - append to an array (`#!cpp Editable` documents only) -- [**insert**](insert.md) - insert an element into an array before a given position (`#!cpp Editable` documents only) + to (editable documents only) +- [**push_back**](push_back.md) - append to an array (editable documents only) +- [**insert**](insert.md) - insert an element into an array before a given position (editable documents only) - [**erase**](erase.md) - remove an object member, an array element, or the value a JSON pointer refers to - (`#!cpp Editable` documents only) + (editable documents only) ## Edits -An editable document (`#!cpp Editable == true`) can be changed after parsing, with [`set`](set.md), +An editable document (`#!cpp view_mode::editable`) can be changed after parsing, with [`set`](set.md), [`push_back`](push_back.md), [`insert`](insert.md), and [`erase`](erase.md); [`json_editable_document`](../json_editable_document.md) and [`ordered_json_editable_document`](../ordered_json_editable_document.md) are the corresponding specializations. A few diff --git a/docs/mkdocs/docs/api/basic_json_document/insert.md b/docs/mkdocs/docs/api/basic_json_document/insert.md index b513dfc0d..883acabda 100644 --- a/docs/mkdocs/docs/api/basic_json_document/insert.md +++ b/docs/mkdocs/docs/api/basic_json_document/insert.md @@ -5,7 +5,7 @@ template view_type insert(view_type array, I idx, V&& value); ``` -Only an **editable** document (`#!cpp Editable == true`, e.g. [`json_editable_document`](../json_editable_document.md)) +Only an **editable** document (`#!cpp view_mode::editable`, e.g. [`json_editable_document`](../json_editable_document.md)) has `insert`; calling it on a read-only `basic_json_document` fails to compile (`#!cpp static_assert`). Inserts `value` into `array` as a new element before position `idx`, which must not be past the end diff --git a/docs/mkdocs/docs/api/basic_json_document/push_back.md b/docs/mkdocs/docs/api/basic_json_document/push_back.md index 37ce10bc5..96e3f113a 100644 --- a/docs/mkdocs/docs/api/basic_json_document/push_back.md +++ b/docs/mkdocs/docs/api/basic_json_document/push_back.md @@ -13,7 +13,7 @@ editable, and it does not have to be `array`'s own document -- which is copied s source document afterward; a `BasicJsonType` value; or anything `BasicJsonType` can be constructed from (numbers, strings, `#!cpp bool`, `#!cpp nullptr`, containers, ...). -Only an **editable** document (`#!cpp Editable == true`, e.g. [`json_editable_document`](../json_editable_document.md)) +Only an **editable** document (`#!cpp view_mode::editable`, e.g. [`json_editable_document`](../json_editable_document.md)) has `push_back`; calling it on a read-only `basic_json_document` fails to compile (`#!cpp static_assert`). ## Template parameters diff --git a/docs/mkdocs/docs/api/basic_json_document/save.md b/docs/mkdocs/docs/api/basic_json_document/save.md index f69d5c9f8..c47295048 100644 --- a/docs/mkdocs/docs/api/basic_json_document/save.md +++ b/docs/mkdocs/docs/api/basic_json_document/save.md @@ -12,7 +12,7 @@ An edited document is written in its *current* state, with its values in documen parser would have produced them for that JSON text: a member [`set`](set.md) added goes at the end, an [`erase`](erase.md)d member leaves no trace, and a float that is not finite (NaN or positive/negative infinity) becomes null, the same substitution [`dump()`](../basic_json_view/dump.md) makes. The same document always saves to -the same bytes -- also across `BasicJsonType` and `#!cpp Editable`, since the image reflects document order and +the same bytes -- also across `BasicJsonType` and the [`view_mode`](../view_mode.md), since the image reflects document order and values only, not which specialization produced them. ## Return value diff --git a/docs/mkdocs/docs/api/basic_json_document/set.md b/docs/mkdocs/docs/api/basic_json_document/set.md index ae26ed516..dd5d99611 100644 --- a/docs/mkdocs/docs/api/basic_json_document/set.md +++ b/docs/mkdocs/docs/api/basic_json_document/set.md @@ -18,7 +18,7 @@ template view_type set(const json_pointer& ptr, V&& value); ``` -Only an **editable** document (`#!cpp Editable == true`, e.g. [`json_editable_document`](../json_editable_document.md)) +Only an **editable** document (`#!cpp view_mode::editable`, e.g. [`json_editable_document`](../json_editable_document.md)) has `set`; calling it on a read-only `basic_json_document` fails to compile (`#!cpp static_assert`). 1. Replaces the value `target` refers to with `value`. diff --git a/docs/mkdocs/docs/api/basic_json_view/basic_json_view.md b/docs/mkdocs/docs/api/basic_json_view/basic_json_view.md index df1e939fa..7ae259287 100644 --- a/docs/mkdocs/docs/api/basic_json_view/basic_json_view.md +++ b/docs/mkdocs/docs/api/basic_json_view/basic_json_view.md @@ -1,20 +1,32 @@ # nlohmann::basic_json_view::basic_json_view ```cpp +// (1) basic_json_view() noexcept = default; + +// (2) only for Mode == view_mode::editable +basic_json_view(const basic_json_view& other) noexcept; ``` -Creates an invalid (discarded) view: [`type()`](type.md) is `#!cpp value_t::discarded` and -[`is_discarded()`](is_discarded.md) is `#!cpp true`. +1. Creates an invalid (discarded) view: [`type()`](type.md) is `#!cpp value_t::discarded` and + [`is_discarded()`](is_discarded.md) is `#!cpp true`. +2. Converts a view of a read-only document into a view of an editable document (implicitly), so that a function + taking a [`json_editable_view`](../json_editable_view.md) accepts both. The result is a view of the same value of + the same read-only document. See [`view_mode`](../view_mode.md) for why there is no conversion the other way. -This is the only constructor a caller can use directly. Every other view is obtained from a +## Parameters + +`other` (in) +: a view of a read-only document with the same `BasicJsonType` + +Apart from these, a view is obtained from a [`basic_json_document`](../basic_json_document/index.md), via [`root()`](../basic_json_document/root.md) or by navigating into a container with [`operator[]`](operator[].md), [`at`](at.md), [`front`](front.md), [`back`](back.md), [`find`](find.md), or iteration. ## Exception safety -No-throw guarantee: this constructor never throws exceptions. +No-throw guarantee: these constructors never throw exceptions. ## Complexity @@ -45,6 +57,7 @@ placeholder for "no value yet" and later be assigned a real view. - [is_discarded](is_discarded.md) - return whether the view is invalid - [root](../basic_json_document/root.md) - the view of a document's root value +- [view_mode](../view_mode.md) - whether a document and its views can be edited ## Version history diff --git a/docs/mkdocs/docs/api/basic_json_view/index.md b/docs/mkdocs/docs/api/basic_json_view/index.md index 5f0d90694..5049e2242 100644 --- a/docs/mkdocs/docs/api/basic_json_view/index.md +++ b/docs/mkdocs/docs/api/basic_json_view/index.md @@ -3,7 +3,7 @@ Defined in header `` ```cpp -template +template class basic_json_view; ``` @@ -37,7 +37,7 @@ Moving the document itself does not invalidate its views: the index is heap-allo `#!cpp operator ValueType()`), and no `#!cpp get_ptr()` or `#!cpp get_ref()`; see [`get`](get.md). `basic_json_view` itself is always read-only -- it never has a `set` or `push_back` of its own. A view of an -**editable** document (`#!cpp Editable == true`) sees every edit made through +**editable** document (`#!cpp view_mode::editable`) sees every edit made through [`basic_json_document::set`](../basic_json_document/set.md) and [`basic_json_document::push_back`](../basic_json_document/push_back.md): once a value is changed, every view that still refers to it -- including ones taken before the change -- reads the new value. See @@ -49,9 +49,10 @@ still refers to it -- including ones taken before the change -- reads the new va : a specialization of [`basic_json`](../basic_json/index.md), matching the [`basic_json_document`](../basic_json_document/index.md) the view was taken from. -`Editable` -: whether the view is of an editable document, matching the [`basic_json_document`](../basic_json_document/index.md) - it was taken from (optional, `#!cpp false` by default). See [Edits](../basic_json_document/index.md#edits). +`Mode` +: a [`view_mode`](../view_mode.md): whether the view is of an editable document, matching the + [`basic_json_document`](../basic_json_document/index.md) it was taken from (optional, `#!cpp view_mode::read_only` by + default). See [Edits](../basic_json_document/index.md#edits). ## Specializations diff --git a/docs/mkdocs/docs/api/json_document.md b/docs/mkdocs/docs/api/json_document.md index cfa22e80b..2da6fe548 100644 --- a/docs/mkdocs/docs/api/json_document.md +++ b/docs/mkdocs/docs/api/json_document.md @@ -3,7 +3,7 @@ Defined in header `` ```cpp -using json_document = basic_json_document; +using json_document = basic_json_document; ``` This type is a [`basic_json_document`](basic_json_document/index.md) of the default [`json`](json.md) diff --git a/docs/mkdocs/docs/api/json_editable_document.md b/docs/mkdocs/docs/api/json_editable_document.md index ec5226b78..2acea534c 100644 --- a/docs/mkdocs/docs/api/json_editable_document.md +++ b/docs/mkdocs/docs/api/json_editable_document.md @@ -3,7 +3,7 @@ Defined in header `` ```cpp -using json_editable_document = basic_json_document; +using json_editable_document = basic_json_document; ``` This type is an **editable** [`basic_json_document`](basic_json_document/index.md) of the default diff --git a/docs/mkdocs/docs/api/json_editable_view.md b/docs/mkdocs/docs/api/json_editable_view.md index ddee4e372..37301d3d6 100644 --- a/docs/mkdocs/docs/api/json_editable_view.md +++ b/docs/mkdocs/docs/api/json_editable_view.md @@ -3,7 +3,7 @@ Defined in header `` ```cpp -using json_editable_view = basic_json_view; +using json_editable_view = basic_json_view; ``` This type is a [`basic_json_view`](basic_json_view/index.md) of a value of a diff --git a/docs/mkdocs/docs/api/json_view.md b/docs/mkdocs/docs/api/json_view.md index 89229ed6f..0998549d9 100644 --- a/docs/mkdocs/docs/api/json_view.md +++ b/docs/mkdocs/docs/api/json_view.md @@ -3,7 +3,7 @@ Defined in header `` ```cpp -using json_view = basic_json_view; +using json_view = basic_json_view; ``` This type is a [`basic_json_view`](basic_json_view/index.md) of a value of a [`json_document`](json_document.md). diff --git a/docs/mkdocs/docs/api/ordered_json_document.md b/docs/mkdocs/docs/api/ordered_json_document.md index b737c2e4d..40039774d 100644 --- a/docs/mkdocs/docs/api/ordered_json_document.md +++ b/docs/mkdocs/docs/api/ordered_json_document.md @@ -3,7 +3,7 @@ Defined in header `` ```cpp -using ordered_json_document = basic_json_document; +using ordered_json_document = basic_json_document; ``` This type is a [`basic_json_document`](basic_json_document/index.md) of the [`ordered_json`](ordered_json.md) diff --git a/docs/mkdocs/docs/api/ordered_json_editable_document.md b/docs/mkdocs/docs/api/ordered_json_editable_document.md index cf86307e5..b47dfc3c2 100644 --- a/docs/mkdocs/docs/api/ordered_json_editable_document.md +++ b/docs/mkdocs/docs/api/ordered_json_editable_document.md @@ -3,7 +3,7 @@ Defined in header `` ```cpp -using ordered_json_editable_document = basic_json_document; +using ordered_json_editable_document = basic_json_document; ``` This type is an **editable** [`basic_json_document`](basic_json_document/index.md) of the diff --git a/docs/mkdocs/docs/api/ordered_json_editable_view.md b/docs/mkdocs/docs/api/ordered_json_editable_view.md index 1c9953d57..a42ca42f5 100644 --- a/docs/mkdocs/docs/api/ordered_json_editable_view.md +++ b/docs/mkdocs/docs/api/ordered_json_editable_view.md @@ -3,7 +3,7 @@ Defined in header `` ```cpp -using ordered_json_editable_view = basic_json_view; +using ordered_json_editable_view = basic_json_view; ``` This type is a [`basic_json_view`](basic_json_view/index.md) of a value of an diff --git a/docs/mkdocs/docs/api/ordered_json_view.md b/docs/mkdocs/docs/api/ordered_json_view.md index 9df72040f..017f2e9a8 100644 --- a/docs/mkdocs/docs/api/ordered_json_view.md +++ b/docs/mkdocs/docs/api/ordered_json_view.md @@ -3,7 +3,7 @@ Defined in header `` ```cpp -using ordered_json_view = basic_json_view; +using ordered_json_view = basic_json_view; ``` This type is a [`basic_json_view`](basic_json_view/index.md) of a value of an diff --git a/docs/mkdocs/docs/api/view_mode.md b/docs/mkdocs/docs/api/view_mode.md new file mode 100644 index 000000000..c0339b5fb --- /dev/null +++ b/docs/mkdocs/docs/api/view_mode.md @@ -0,0 +1,88 @@ +# nlohmann::view_mode + +Defined in header `` + +```cpp +enum class view_mode : std::uint8_t +{ + read_only, + editable +}; +``` + +The second template parameter of [`basic_json_document`](basic_json_document/index.md) and +[`basic_json_view`](basic_json_view/index.md): whether a document can be changed after parsing, and whether a view is +of such a document. + +`read_only` +: the default: the document is parsed once and then only read. It carries none of the bookkeeping edits need, and + calling [`set`](basic_json_document/set.md), [`push_back`](basic_json_document/push_back.md), + [`insert`](basic_json_document/insert.md), or [`erase`](basic_json_document/erase.md) on it fails to compile. + +`editable` +: the document also offers [`set`](basic_json_document/set.md), [`push_back`](basic_json_document/push_back.md), + [`insert`](basic_json_document/insert.md), and [`erase`](basic_json_document/erase.md); its views see every edit, + see [Edits](basic_json_document/index.md#edits). + +The shortcuts name both modes: [`json_document`](json_document.md) and [`json_view`](json_view.md) are read-only, +[`json_editable_document`](json_editable_document.md) and [`json_editable_view`](json_editable_view.md) are editable +(and likewise for `ordered_json`). + +## Notes + +The mode is a compile-time property of the document's and the view's type, not a setting of a call. A read-only view +reads a container in place, while a view of an editable document follows the links that edits leave behind. A view of +a read-only document converts implicitly to a view of an editable document of the same `BasicJsonType`: a read-only +document has no edits, so the converted view reads it like an editable document before its first edit (at the cost of +one check per access). There is no conversion the other way, because a read-only view of an editable document would +read wrong values after the next edit. + +A function that takes a [`json_editable_view`](json_editable_view.md) therefore accepts the views of both modes. A +function template deduces the mode from its argument instead, without conversions, so it names the modes it accepts in +its signature: + +```cpp +// views of editable documents only +template +void f(basic_json_view v); + +// views of read-only documents only +template +void f(basic_json_view v); + +// either +template +void f(basic_json_view v); +``` + +A function that takes a [`json_view`](json_view.md) accepts neither an [`ordered_json_view`](ordered_json_view.md) +nor a [`json_editable_view`](json_editable_view.md). Views of the two modes compare with +[`operator==`](basic_json_view/operator_eq.md). + +## Examples + +??? example + + The example below shows a function template that accepts the views of both modes, one that accepts only views of + editable documents, and a function taking a `json_editable_view` that also accepts a view of a read-only document. + + ```cpp + --8<-- "examples/view_mode.cpp" + ``` + + Output: + + ``` + --8<-- "examples/view_mode.output" + ``` + +## See also + +- [basic_json_document](basic_json_document/index.md) - a parsed JSON text +- [basic_json_view](basic_json_view/index.md) - a handle to one value of a document +- [json_editable_document](json_editable_document.md) - an editable document of `json` +- [(constructor)](basic_json_view/basic_json_view.md) - the conversion from a read-only to an editable view + +## Version history + +Added in version 3.13.0. diff --git a/docs/mkdocs/docs/examples/ordered_json_editable_document.cpp b/docs/mkdocs/docs/examples/ordered_json_editable_document.cpp index 9d7821066..7089466e4 100644 --- a/docs/mkdocs/docs/examples/ordered_json_editable_document.cpp +++ b/docs/mkdocs/docs/examples/ordered_json_editable_document.cpp @@ -5,7 +5,7 @@ using nlohmann::ordered_json_editable_document; int main() { - // ordered_json_editable_document is basic_json_document + // ordered_json_editable_document is basic_json_document ordered_json_editable_document doc = ordered_json_editable_document::parse(R"({"z": 1, "a": 2, "m": 3})"); doc.set(doc.root(), "b", 4); // set() always appends a new member at the end diff --git a/docs/mkdocs/docs/examples/view_mode.cpp b/docs/mkdocs/docs/examples/view_mode.cpp new file mode 100644 index 000000000..5cdd7718b --- /dev/null +++ b/docs/mkdocs/docs/examples/view_mode.cpp @@ -0,0 +1,36 @@ +#include +#include + +using nlohmann::basic_json_view; +using nlohmann::view_mode; + +// accepts the views of read-only and of editable documents, of json and of ordered_json +template +std::size_t count_members(basic_json_view v) +{ + return v.is_object() ? v.size() : 0; +} + +// accepts only the views of editable documents +template +bool is_edited(basic_json_view v) +{ + return v.is_string() && v.get_string() == "edited"; +} + +// a function (no template) taking a json_editable_view also accepts the view of a read-only document +std::size_t size_of(nlohmann::json_editable_view v) +{ + return v.size(); +} + +int main() +{ + const nlohmann::json_document doc = nlohmann::json_document::parse(R"({"a": 1, "b": 2})"); + nlohmann::json_editable_document edoc = nlohmann::json_editable_document::parse(R"({"a": 1})"); + edoc.set(edoc.root()["a"], "edited"); + + std::cout << count_members(doc.root()) << ' ' << count_members(edoc.root()) << '\n'; + std::cout << std::boolalpha << is_edited(edoc.root()["a"]) << '\n'; + std::cout << size_of(doc.root()) << ' ' << size_of(edoc.root()) << '\n'; +} diff --git a/docs/mkdocs/docs/examples/view_mode.output b/docs/mkdocs/docs/examples/view_mode.output new file mode 100644 index 000000000..0e56385a0 --- /dev/null +++ b/docs/mkdocs/docs/examples/view_mode.output @@ -0,0 +1,3 @@ +2 1 +true +2 1 diff --git a/docs/mkdocs/docs/features/json_view.md b/docs/mkdocs/docs/features/json_view.md index 4f0938273..4b5498af5 100644 --- a/docs/mkdocs/docs/features/json_view.md +++ b/docs/mkdocs/docs/features/json_view.md @@ -252,14 +252,14 @@ the stream's `width`/`fill` for indentation. ## Editing a document Everything above is read-only: a `json_document`/`json_view` lets you look at a parsed text without copying it, but -not change it. [`basic_json_document`](../api/basic_json_document/index.md) -- more conveniently +not change it. [`basic_json_document`](../api/basic_json_document/index.md) -- more conveniently spelled [`json_editable_document`](../api/json_editable_document.md) or [`ordered_json_editable_document`](../api/ordered_json_editable_document.md) -- also lets you [`set`](../api/basic_json_document/set.md) a value, [`push_back`](../api/basic_json_document/push_back.md) onto or [`insert`](../api/basic_json_document/insert.md) into an array, and [`erase`](../api/basic_json_document/erase.md) an object member or an array element, still without ever building a `basic_json` tree for parts you do not touch. -`#!cpp Editable` defaults to `#!cpp false`, so `json_document`/`ordered_json_document` are unaffected -- they carry +The [mode](../api/view_mode.md) defaults to `#!cpp view_mode::read_only`, so `json_document`/`ordered_json_document` are unaffected -- they carry none of the bookkeeping edits need, and calling `set`/`push_back`/`insert`/`erase` on one is a compile error, not a runtime one. diff --git a/docs/mkdocs/mkdocs.yml b/docs/mkdocs/mkdocs.yml index f2b99cf3a..66bac7e80 100644 --- a/docs/mkdocs/mkdocs.yml +++ b/docs/mkdocs/mkdocs.yml @@ -411,6 +411,7 @@ nav: - 'ordered_json_editable_view': api/ordered_json_editable_view.md - 'ordered_json_view': api/ordered_json_view.md - 'ordered_map': api/ordered_map.md + - 'view_mode': api/view_mode.md - macros: - 'Overview': api/macros/index.md - 'JSON_ASSERT': api/macros/json_assert.md diff --git a/include/nlohmann/detail/view/builder.hpp b/include/nlohmann/detail/view/builder.hpp index 3e4ea268a..f7b560f50 100644 --- a/include/nlohmann/detail/view/builder.hpp +++ b/include/nlohmann/detail/view/builder.hpp @@ -639,11 +639,11 @@ run_found: switch (cur()) { case '{': - open(value_t::object); + open(node_kind::object); ++p; goto obj_first; case '[': - open(value_t::array); + open(node_kind::array); ++p; goto arr_first; default: @@ -665,11 +665,11 @@ run_found: if (NLOHMANN_VIEW_UNLIKELY(!string())) { return false; } \ break; \ case '{': \ - open(value_t::object); \ + open(node_kind::object); \ ++p; \ goto obj_first; \ case '[': \ - open(value_t::array); \ + open(node_kind::array); \ ++p; \ goto arr_first; \ case '-': \ @@ -886,17 +886,17 @@ root_done: NLOHMANN_VIEW_ALWAYS_INLINE bool literal_true() { - return literal("true", value_t::boolean, node_flags::is_true); + return literal("true", node_kind::boolean, node_flags::is_true); } NLOHMANN_VIEW_ALWAYS_INLINE bool literal_false() { - return literal("false", value_t::boolean, 0); + return literal("false", node_kind::boolean, 0); } NLOHMANN_VIEW_ALWAYS_INLINE bool literal_null() { - return literal("null", value_t::null, 0); + return literal("null", node_kind::null, 0); } /// skip whitespace (and comments); false on a malformed comment @@ -962,7 +962,7 @@ indent_done: /// append a node: (kind, flags, extra, off) and the second word (len, or /// an integer's value); two stores on little-endian targets - NLOHMANN_VIEW_ALWAYS_INLINE node* push_node(value_t k, std::uint8_t flags, std::uint16_t extra, std::size_t off, std::uint64_t second) + NLOHMANN_VIEW_ALWAYS_INLINE node* push_node(node_kind k, std::uint8_t flags, std::uint16_t extra, std::size_t off, std::uint64_t second) { if (NLOHMANN_VIEW_UNLIKELY(out == cap)) { @@ -978,7 +978,7 @@ indent_done: std::memcpy(reinterpret_cast(n), &first, 8); std::memcpy(reinterpret_cast(n) + 8, &second, 8); #else - n->kind = static_cast(k); + n->kind = k; n->flags = flags; n->extra = extra; n->off = static_cast(off); @@ -990,7 +990,7 @@ indent_done: return n; } - NLOHMANN_VIEW_ALWAYS_INLINE void open(value_t k) + NLOHMANN_VIEW_ALWAYS_INLINE void open(node_kind k) { // (base is read after push_node(), which moves the node array when it // grows; in one expression the order of the two is unspecified) @@ -1017,7 +1017,7 @@ indent_done: max_depth = (std::max)(max_depth, depth); cur_idx = idx; cur_count = 0; - cur_is_object = k == value_t::object; + cur_is_object = k == node_kind::object; } NLOHMANN_VIEW_ALWAYS_INLINE void close() @@ -1047,7 +1047,7 @@ indent_done: /// the literal `text` at p (N counts the terminating NUL) template - NLOHMANN_VIEW_ALWAYS_INLINE bool literal(const char (&text)[N], value_t k, std::uint8_t flags) // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays): a string literal + NLOHMANN_VIEW_ALWAYS_INLINE bool literal(const char (&text)[N], node_kind k, std::uint8_t flags) // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays): a string literal { const std::size_t n = N - 1; if (NLOHMANN_VIEW_UNLIKELY(e - p < static_cast(n) || std::memcmp(p, text, n) != 0)) @@ -1126,23 +1126,23 @@ indent_done: is_float = true; } - value_t kind = value_t::number_float; + node_kind kind = node_kind::number_float; if (!is_float) { // integers that do not fit become floats, as in parse() if (negative) { - kind = digits_exceed(int_start, int_digits, "9223372036854775808") ? value_t::number_float : value_t::number_integer; + kind = digits_exceed(int_start, int_digits, "9223372036854775808") ? node_kind::number_float : node_kind::number_integer; } else { - kind = digits_exceed(int_start, int_digits, "18446744073709551615") ? value_t::number_float : value_t::number_unsigned; + kind = digits_exceed(int_start, int_digits, "18446744073709551615") ? node_kind::number_float : node_kind::number_unsigned; } } // parse() rejects floats that overflow; only numbers whose magnitude // could reach the largest FloatType (1e308 for double, 1e38 for // float) need the conversion - if (NLOHMANN_VIEW_UNLIKELY(static_cast(int_digits) + exponent > std::numeric_limits::max_exponent10 - 8 && kind == value_t::number_float)) + if (NLOHMANN_VIEW_UNLIKELY(static_cast(int_digits) + exponent > std::numeric_limits::max_exponent10 - 8 && kind == node_kind::number_float)) { if (builder::float_overflows(s, p)) { @@ -1152,7 +1152,7 @@ indent_done: } const auto layout = static_cast((int_digits < 255 ? int_digits : 255) | ((frac_digits < 255 ? frac_digits : 255) << 8)); auto second = static_cast(p - s); - if (kind != value_t::number_float) + if (kind != node_kind::number_float) { // integers are converted now, while their digits are in cache const std::uint64_t m = int_digits <= 19 ? parse_upto19(int_start, static_cast(int_digits), e) @@ -1175,7 +1175,7 @@ indent_done: p = scan_string_run(p, e); if (NLOHMANN_VIEW_LIKELY(cur() == '"')) { - push_node(value_t::string, 0, 0, static_cast(s - b), static_cast(p - s)); + push_node(node_kind::string, 0, 0, static_cast(s - b), static_cast(p - s)); ++p; return true; } @@ -1185,7 +1185,7 @@ indent_done: return false; } p = r.p; - push_node(value_t::string, node_flags::escaped, 0, r.start, r.len); + push_node(node_kind::string, node_flags::escaped, 0, r.start, r.len); return true; } }; diff --git a/include/nlohmann/detail/view/document_data.hpp b/include/nlohmann/detail/view/document_data.hpp index 55f9d8c76..afef5a7a2 100644 --- a/include/nlohmann/detail/view/document_data.hpp +++ b/include/nlohmann/detail/view/document_data.hpp @@ -232,7 +232,7 @@ struct document_data /// following loads. static NLOHMANN_VIEW_ALWAYS_INLINE const node* deref(const node* n) noexcept { - return NLOHMANN_VIEW_LIKELY(n->kind != kind_link) ? n : follow_link(n); + return NLOHMANN_VIEW_LIKELY(n->kind != node_kind::link) ? n : follow_link(n); } static NLOHMANN_VIEW_NOINLINE const node* follow_link(const node* n) noexcept diff --git a/include/nlohmann/detail/view/edit.hpp b/include/nlohmann/detail/view/edit.hpp index bad8d9da3..f47bf5985 100644 --- a/include/nlohmann/detail/view/edit.hpp +++ b/include/nlohmann/detail/view/edit.hpp @@ -26,12 +26,10 @@ #include #include #include +#include NLOHMANN_JSON_NAMESPACE_BEGIN -template -class basic_json_view; - namespace detail { namespace view @@ -120,15 +118,15 @@ class editor View set(const View& object, string_view_t key, V&& value) { node* const o = own(object); - if (o->kind != static_cast(value_t::object) && o->kind != static_cast(value_t::null)) + if (o->kind != node_kind::object && o->kind != node_kind::null) { throw_type_error(exception_id::subscript_wrong_type, "cannot use operator[] with a string argument with ", object.type_name()); } check_utf8(key.data(), key.size()); const encoded e = encode(std::forward(value)); - if (o->kind == static_cast(value_t::null)) + if (o->kind == node_kind::null) { - become_empty(o, value_t::object); + become_empty(o, node_kind::object); } // an existing member: assign it (and drop later duplicates, so that // lookups, iteration, and materialize() agree) @@ -171,7 +169,7 @@ class editor View set(const View& array, std::size_t idx, V&& value) { node* const a = own(array); - if (a->kind != static_cast(value_t::array)) + if (a->kind != node_kind::array) { throw_type_error(exception_id::subscript_wrong_type, "cannot use operator[] with a numeric argument with ", array.type_name()); } @@ -188,14 +186,14 @@ class editor View push_back(const View& array, V&& value) { node* const a = own(array); - if (a->kind != static_cast(value_t::array) && a->kind != static_cast(value_t::null)) + if (a->kind != node_kind::array && a->kind != node_kind::null) { throw_type_error(exception_id::push_back_wrong_type, "cannot use push_back() with ", array.type_name()); } const encoded e = encode(std::forward(value)); - if (a->kind == static_cast(value_t::null)) + if (a->kind == node_kind::null) { - become_empty(a, value_t::array); + become_empty(a, node_kind::array); } node* const slot = new_slot(e); node* const h = block_of(m_doc, a, 1); @@ -212,7 +210,7 @@ class editor View insert(const View& array, std::size_t idx, V&& value) { node* const a = own(array); - if (a->kind != static_cast(value_t::array)) + if (a->kind != node_kind::array) { throw_type_error(exception_id::insert_wrong_type, "cannot use insert() with ", array.type_name()); } @@ -232,7 +230,7 @@ class editor std::size_t erase(const View& object, string_view_t key) { node* const o = own(object); - if (o->kind != static_cast(value_t::object)) + if (o->kind != node_kind::object) { throw_type_error(exception_id::erase_wrong_type, "cannot use erase() with ", object.type_name()); } @@ -250,7 +248,7 @@ class editor void erase(const View& array, std::size_t idx) { node* const a = own(array); - if (a->kind != static_cast(value_t::array)) + if (a->kind != node_kind::array) { throw_type_error(exception_id::erase_wrong_type, "cannot use erase() with ", array.type_name()); } @@ -324,11 +322,11 @@ class editor } /// turn a null into an empty array/object in place - static void become_empty(node* n, value_t k) noexcept + static void become_empty(node* n, node_kind k) noexcept { const std::uint8_t linked = n->flags & node_flags::linked; *n = node{}; - n->kind = static_cast(k); + n->kind = k; n->flags = static_cast(node_flags::is_new | linked); n->next = 1; } @@ -405,8 +403,8 @@ class editor template struct is_view : std::false_type {}; - template - struct is_view> : std::true_type {}; + template + struct is_view> : std::true_type {}; template encoded encode(V&& v) @@ -421,8 +419,8 @@ class editor } /// a view of any document (copied; nothing is shared with it) - template - encoded encode_impl(const basic_json_view& v, encode_tag<0> /*view*/) + template + encoded encode_impl(const basic_json_view& v, encode_tag<0> /*view*/) { if (NLOHMANN_VIEW_UNLIKELY(v.m_node == nullptr)) { @@ -434,8 +432,8 @@ class editor r.scalar = copy_scalar(*v.m_doc, *v.m_node); return r; } - r.region = alloc_nodes(m_doc, count_nodes(*v.m_doc, v.m_node)); - fill_nodes(*v.m_doc, v.m_node, r.region); + r.region = alloc_nodes(m_doc, count_nodes(*v.m_doc, v.m_node)); + fill_nodes(*v.m_doc, v.m_node, r.region); edit_state_of(m_doc).regions.emplace(r.region, nullptr); return r; } @@ -457,14 +455,14 @@ class editor encoded encode_impl(std::nullptr_t /*unused*/, encode_tag<2> /*null*/) { encoded r; - r.scalar = plain_node(value_t::null); + r.scalar = plain_node(node_kind::null); return r; } encoded encode_impl(bool b, encode_tag<3> /*boolean*/) { encoded r; - r.scalar = plain_node(value_t::boolean); + r.scalar = plain_node(node_kind::boolean); r.scalar.flags = static_cast(r.scalar.flags | (b ? node_flags::is_true : 0)); return r; } @@ -493,10 +491,10 @@ class editor return encode_impl(BasicJsonType(std::forward(x)), encode_tag<1> {}); } - static node plain_node(value_t k) noexcept + static node plain_node(node_kind k) noexcept { node n{}; - n.kind = static_cast(k); + n.kind = k; n.flags = node_flags::is_new; return n; } @@ -510,19 +508,19 @@ class editor template node number_node(T x, std::integral_constant /*signed*/) { - return integer_node(static_cast(static_cast(x)), value_t::number_integer); + return integer_node(static_cast(static_cast(x)), node_kind::number_integer); } template node number_node(T x, std::integral_constant /*unsigned*/) { - return integer_node(static_cast(x), value_t::number_unsigned); + return integer_node(static_cast(x), node_kind::number_unsigned); } /// an integer with its canonical token in the edit arena - node integer_node(std::uint64_t bits, value_t k) + node integer_node(std::uint64_t bits, node_kind k) { - const bool negative = k == value_t::number_integer && static_cast(bits) < 0; + const bool negative = k == node_kind::number_integer && static_cast(bits) < 0; std::uint64_t magnitude = negative ? 0 - bits : bits; std::array buf{}; char* p = buf.data() + buf.size(); // NOLINT(misc-const-correctness): digits are written through p @@ -541,7 +539,7 @@ class editor n.flags = static_cast(n.flags | node_flags::edited); n.off = append_text(m_doc, p, len); // number_length() adds one for the sign of number_integer nodes - n.extra = static_cast(k == value_t::number_integer ? len - 1 : len); + n.extra = static_cast(k == node_kind::number_integer ? len - 1 : len); set_integer_bits(n, bits); return n; } @@ -563,7 +561,7 @@ class editor { text = BasicJsonType(x).dump(); } - node n = plain_node(value_t::number_float); + node n = plain_node(node_kind::number_float); n.flags = static_cast(n.flags | node_flags::edited); n.extra = 0xFFFFu; // (the digit layout is not recorded) n.off = append_text(m_doc, text.data(), text.size()); @@ -578,7 +576,7 @@ class editor { throw_out_of_range(exception_id::document_too_large, "strings of 4 GiB or more are not supported by json_document"); // LCOV_EXCL_LINE } - node n = plain_node(value_t::string); + node n = plain_node(node_kind::string); n.flags = static_cast(n.flags | node_flags::edited); n.off = append_text(m_doc, s, len); n.len = static_cast(len); @@ -600,10 +598,10 @@ class editor return string_node(from.str(n), n.len); case value_t::number_integer: case value_t::number_unsigned: - return integer_node(integer_bits(n), static_cast(n.kind)); + return integer_node(integer_bits(n), n.kind); case value_t::number_float: { - node r = plain_node(value_t::number_float); + node r = plain_node(node_kind::number_float); r.flags = static_cast(r.flags | node_flags::edited); r.off = append_text(m_doc, from.str(n), n.len); r.len = n.len; @@ -612,7 +610,7 @@ class editor } case value_t::boolean: { - node r = plain_node(value_t::boolean); + node r = plain_node(node_kind::boolean); r.flags = static_cast(r.flags | (n.flags & node_flags::is_true)); return r; } @@ -622,7 +620,7 @@ class editor case value_t::binary: case value_t::discarded: default: - return plain_node(value_t::null); + return plain_node(node_kind::null); } } @@ -631,17 +629,17 @@ class editor switch (j.type()) { case value_t::null: - return plain_node(value_t::null); + return plain_node(node_kind::null); case value_t::boolean: { - node r = plain_node(value_t::boolean); + node r = plain_node(node_kind::boolean); r.flags = static_cast(r.flags | (j.template get() ? node_flags::is_true : 0)); return r; } case value_t::number_integer: - return integer_node(static_cast(static_cast(j.template get())), value_t::number_integer); + return integer_node(static_cast(static_cast(j.template get())), node_kind::number_integer); case value_t::number_unsigned: - return integer_node(static_cast(j.template get()), value_t::number_unsigned); + return integer_node(static_cast(j.template get()), node_kind::number_unsigned); case value_t::number_float: return float_node(j.template get()); case value_t::string: @@ -681,7 +679,7 @@ class editor ++r; if (is_container(*n)) { - open.push_back(frame{walk::first(d, n), walk::end(d, n), n->kind == static_cast(value_t::object)}); + open.push_back(frame{walk::first(d, n), walk::end(d, n), n->kind == node_kind::object}); } // the next value: close finished containers, then step over the key for (;;) @@ -728,9 +726,9 @@ class editor if (is_container(*n)) { node* const self = out++; - *self = plain_node(static_cast(n->kind)); + *self = plain_node(n->kind); self->len = n->len; - open.push_back(frame{walk::first(d, n), walk::end(d, n), n->kind == static_cast(value_t::object), self}); + open.push_back(frame{walk::first(d, n), walk::end(d, n), n->kind == node_kind::object, self}); } else { @@ -819,7 +817,7 @@ class editor if (n->is_structured()) { node* const self = out++; - *self = plain_node(n->type()); + *self = plain_node(static_cast(n->type())); self->len = static_cast(n->size()); open.push_back(frame{n->cbegin(), n->cend(), n->is_object(), self}); } diff --git a/include/nlohmann/detail/view/edit_storage.hpp b/include/nlohmann/detail/view/edit_storage.hpp index 9a45bfebd..e5b7ddb90 100644 --- a/include/nlohmann/detail/view/edit_storage.hpp +++ b/include/nlohmann/detail/view/edit_storage.hpp @@ -27,7 +27,7 @@ // live in chunks that never move; strings and number tokens written by edits // live in the edit arena. An array/object whose elements change gets // node_flags::moved: its elements then live in a separate sequence (a header -// node, then the entries), whose entries link to the values (kind_link). +// node, then the entries), whose entries link to the values (node_kind::link). NLOHMANN_JSON_NAMESPACE_BEGIN namespace detail @@ -185,7 +185,7 @@ inline node* block_of(document_data& d, node* n, std::size_t extra) return nh; } reserve_moved(d); // (so that set_moved() below cannot throw: the links are marked before) - const bool object = n->kind == static_cast(value_t::object); + const bool object = n->kind == node_kind::object; const std::size_t used = 1 + (static_cast(n->len) * (object ? 2 : 1)); const std::size_t cap = used + extra; node* const h = alloc_nodes(d, cap); @@ -251,7 +251,7 @@ inline node* find_parent(const document_data& d, const node* target) { return nullptr; // LCOV_EXCL_LINE (the value is inside c) } - const bool object = c->kind == static_cast(value_t::object); + const bool object = c->kind == node_kind::object; const node* down = nullptr; for (const node* p = d.first_child_edited(c), *e = d.child_end_edited(c); p != e;) { diff --git a/include/nlohmann/detail/view/image.hpp b/include/nlohmann/detail/view/image.hpp index af9994712..c13d269e1 100644 --- a/include/nlohmann/detail/view/image.hpp +++ b/include/nlohmann/detail/view/image.hpp @@ -117,7 +117,7 @@ inline void compact_nodes(const document_data& d, std::size_t arena_size, std::v r.off = (v->flags & (node_flags::moved | node_flags::is_new)) != 0 ? 0 : v->off; r.len = 0; // counted below r.next = 0; // set when the container is complete - stack.push_back(frame{d.first_child_edited(v), d.child_end_edited(v), out.size(), 0, v->kind == static_cast(value_t::object)}); + stack.push_back(frame{d.first_child_edited(v), d.child_end_edited(v), out.size(), 0, v->kind == node_kind::object}); break; case value_t::string: r = string_node(*v); @@ -138,7 +138,7 @@ inline void compact_nodes(const document_data& d, std::size_t arena_size, std::v if (t[0] == 'n' || t[0] == 'i' || (v->len > 1 && t[1] == 'i')) { r = node{}; // nan and infinity: null, as dump() writes them - r.kind = static_cast(value_t::null); + r.kind = node_kind::null; break; } r.off = static_cast(d.size + text_tail.size()); @@ -221,7 +221,7 @@ inline std::vector save_image(const document_data& d) // extra of an object, and the same document saves to the same bytes for (std::size_t i = 0; i < count; ++i) { - if (nodes[i].kind == static_cast(value_t::object) && nodes[i].extra != 0) + if (nodes[i].kind == node_kind::object && nodes[i].extra != 0) { std::memset(o + (i * sizeof(node)) + offsetof(node, extra), 0, sizeof(nodes[i].extra)); } @@ -337,7 +337,7 @@ inline bool float_token_finite(const unsigned char* s, std::size_t len, const nu if (static_cast(t.int_digits) + t.exponent > 300) { node n{}; - n.kind = static_cast(value_t::number_float); + n.kind = node_kind::number_float; n.len = static_cast(len); const auto v = float_value(reinterpret_cast(s), n); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) return v <= (std::numeric_limits::max)() && v >= -(std::numeric_limits::max)(); @@ -358,7 +358,7 @@ inline bool check_integer(const node& n, const unsigned char* text) } // the token's value is the stored one; number_integer nodes of edits can // be non-negative (as basic_json keeps the type of a value) - const bool integer = n.kind == static_cast(value_t::number_integer); + const bool integer = n.kind == node_kind::number_integer; if (t.is_float || t.int_digits > 20 || (t.negative && !integer)) { return false; @@ -540,7 +540,7 @@ inline bool check_image(const node* nodes, std::size_t count, const unsigned cha { return false; } - if (n.kind != static_cast(value_t::number_float)) + if (n.kind != node_kind::number_float) { return (n.extra >> 8u) == 0; } @@ -581,7 +581,7 @@ inline bool check_image(const node* nodes, std::size_t count, const unsigned cha const node& n = nodes[i]; if (!stack.empty() && stack.back().object && stack.back().expect_key) { - if (n.kind != static_cast(value_t::string) || !check_string(n)) + if (n.kind != node_kind::string || !check_string(n)) { return false; } @@ -622,7 +622,7 @@ inline bool check_image(const node* nodes, std::size_t count, const unsigned cha { // (the contents of float tokens are checked later; the token of an // integer has at most 256 characters) - if (n.kind == static_cast(value_t::number_float)) + if (n.kind == node_kind::number_float) { floats.push_back(float_range{n.off, n.len, n.extra}); } @@ -640,7 +640,7 @@ inline bool check_image(const node* nodes, std::size_t count, const unsigned cha { return false; } - stack.push_back(frame{i + n.next, n.len, 0, n.kind == static_cast(value_t::object), true}); + stack.push_back(frame{i + n.next, n.len, 0, n.kind == node_kind::object, true}); complete = false; break; } @@ -729,7 +729,7 @@ inline void load_image(document_data& d, const std::uint8_t* image, std::size_t for (std::size_t i = 0; i < count; ++i) { const node& n = d.nodes[i]; - if (n.kind == static_cast(value_t::object) && n.len >= document_data::index_min_members) + if (n.kind == node_kind::object && n.len >= document_data::index_min_members) { d.large_objects.push_back(static_cast(i)); } diff --git a/include/nlohmann/detail/view/lookup.hpp b/include/nlohmann/detail/view/lookup.hpp index e2038a956..f288927bb 100644 --- a/include/nlohmann/detail/view/lookup.hpp +++ b/include/nlohmann/detail/view/lookup.hpp @@ -182,7 +182,7 @@ const node* element_at(const document_data& d, const node* array, std::size_t id template const node* last_child(const document_data& d, const node* container) noexcept { - const std::size_t value_offset = container->kind == static_cast(value_t::object) ? 1 : 0; + const std::size_t value_offset = container->kind == node_kind::object ? 1 : 0; const node* const end = navigation::end(d, container); const node* last = navigation::first(d, container); if (value_offset == 0 && container->next == container->len + 1u && (!navigation::editable() || (container->flags & node_flags::moved) == 0)) diff --git a/include/nlohmann/detail/view/materialize.hpp b/include/nlohmann/detail/view/materialize.hpp index 62adb30a9..1bab0e896 100644 --- a/include/nlohmann/detail/view/materialize.hpp +++ b/include/nlohmann/detail/view/materialize.hpp @@ -101,7 +101,7 @@ BasicJsonType materialize(const document_data& d, const node* n) case value_t::object: case value_t::array: { - const bool object = n->kind == static_cast(value_t::object); + const bool object = n->kind == node_kind::object; if (object) { sax.start_object(n->len); diff --git a/include/nlohmann/detail/view/node.hpp b/include/nlohmann/detail/view/node.hpp index 7a05c8e0a..8987a8860 100644 --- a/include/nlohmann/detail/view/node.hpp +++ b/include/nlohmann/detail/view/node.hpp @@ -41,9 +41,22 @@ struct node_flags static constexpr std::uint8_t linked = 32; ///< an entry of a moved sequence links to this value (editable documents): its extent in the parsed layout no longer matters }; -/// kind of an entry of an edited sequence that stands for a value stored -/// elsewhere (the address of the value's node is kept in the len/next bytes) -constexpr std::uint8_t kind_link = 10; +/// the kind of a node: the value_t of its value, or link +enum class node_kind : std::uint8_t +{ + null = static_cast(value_t::null), + object = static_cast(value_t::object), + array = static_cast(value_t::array), + string = static_cast(value_t::string), // NOLINT(build/include_what_you_use): an enumerator, not std::string + boolean = static_cast(value_t::boolean), + number_integer = static_cast(value_t::number_integer), + number_unsigned = static_cast(value_t::number_unsigned), + number_float = static_cast(value_t::number_float), + /// an entry of an edited sequence that stands for a value stored elsewhere + /// (the address of the value's node is kept in the len/next bytes); placed + /// after value_t::discarded, which no node ever has + link = static_cast(value_t::discarded) + 1, +}; /// One entry of the flat index, in document order. An object's members are /// stored as key node followed by the value's subtree. Integers keep their @@ -51,7 +64,7 @@ constexpr std::uint8_t kind_link = 10; /// always the next one, and the token length follows from `extra`). struct node { - std::uint8_t kind; ///< value_t, or kind_link + node_kind kind; ///< the kind of the value, or link std::uint8_t flags; ///< node_flags std::uint16_t extra; ///< numbers: integer digits (low byte) and fraction digits (high byte), 255 = "many"; objects: number of the object's key hash index (1-based, 0 = none); otherwise 0 std::uint32_t off; ///< source offset (string content, number token, literal, bracket); arena offset if escaped/edited; number of the element sequence if moved @@ -60,13 +73,13 @@ struct node }; static_assert(sizeof(node) == 16, "node must stay 16 bytes"); -// the node kinds are value_t values; the test below (object and array are 1 -// and 2) and those of the number kinds depend on this numbering -static_assert(static_cast(value_t::null) == 0 && static_cast(value_t::object) == 1 - && static_cast(value_t::array) == 2 && static_cast(value_t::string) == 3 - && static_cast(value_t::boolean) == 4 && static_cast(value_t::number_integer) == 5 - && static_cast(value_t::number_unsigned) == 6 && static_cast(value_t::number_float) == 7, - "the node format depends on the numbering of value_t"); +// images store the node kinds as bytes, and the test below (object and array +// are 1 and 2) and those of the number kinds depend on this numbering +static_assert(static_cast(node_kind::null) == 0 && static_cast(node_kind::object) == 1 + && static_cast(node_kind::array) == 2 && static_cast(node_kind::string) == 3 + && static_cast(node_kind::boolean) == 4 && static_cast(node_kind::number_integer) == 5 + && static_cast(node_kind::number_unsigned) == 6 && static_cast(node_kind::number_float) == 7, + "the node format depends on the numbering of node_kind"); NLOHMANN_VIEW_ALWAYS_INLINE bool is_container(const node& n) noexcept { @@ -86,7 +99,7 @@ inline void make_link(node& n, node* target) noexcept { target->flags = static_cast(target->flags | node_flags::linked); n = node{}; - n.kind = kind_link; + n.kind = node_kind::link; std::memcpy(reinterpret_cast(&n) + 8, static_cast(&target), sizeof(const node*)); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) } @@ -116,8 +129,8 @@ NLOHMANN_VIEW_ALWAYS_INLINE void set_integer_bits(node& n, std::uint64_t v) noex /// token length of a number node NLOHMANN_VIEW_ALWAYS_INLINE std::uint32_t number_length(const node& n) noexcept { - return n.kind == static_cast(value_t::number_float) ? n.len - : (n.extra & 0xFFu) + (n.kind == static_cast(value_t::number_integer) ? 1u : 0u); + return n.kind == node_kind::number_float ? n.len + : (n.extra & 0xFFu) + (n.kind == node_kind::number_integer ? 1u : 0u); } /// estimated number of nodes for an input of `size` bytes (one node per ~12 diff --git a/include/nlohmann/detail/view/serializer.hpp b/include/nlohmann/detail/view/serializer.hpp index 76df8f89b..b41ac683f 100644 --- a/include/nlohmann/detail/view/serializer.hpp +++ b/include/nlohmann/detail/view/serializer.hpp @@ -874,7 +874,7 @@ class view_serializer case value_t::object: case value_t::array: { - const bool object = n->kind == static_cast(value_t::object); + const bool object = n->kind == node_kind::object; room(8); if (n->len == 0) { diff --git a/include/nlohmann/detail/view/view_mode.hpp b/include/nlohmann/detail/view/view_mode.hpp new file mode 100644 index 000000000..3aeccd0b9 --- /dev/null +++ b/include/nlohmann/detail/view/view_mode.hpp @@ -0,0 +1,52 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#pragma once + +#include // uint8_t +#include // false_type, true_type + +#include + +NLOHMANN_JSON_NAMESPACE_BEGIN + +/// whether a basic_json_document and its views can be edited +enum class view_mode : std::uint8_t +{ + read_only, ///< parsed once, then only read (the default) + editable, ///< also has set(), push_back(), insert(), and erase() +}; + +template +class basic_json_view; + +namespace detail +{ +namespace view +{ + +/// whether two view modes differ +template +struct is_other_mode : std::true_type {}; + +template +struct is_other_mode : std::false_type {}; + +/// whether a view of mode From converts implicitly to one of mode To: a +/// read-only document has no edits, so a view of it is read correctly as a +/// view of an editable document that has not been edited yet +template +struct is_widening_mode : std::false_type {}; + +template<> +struct is_widening_mode : std::true_type {}; + +} // namespace view +} // namespace detail + +NLOHMANN_JSON_NAMESPACE_END diff --git a/include/nlohmann/json_view.hpp b/include/nlohmann/json_view.hpp index 85026008a..42a8cb99e 100644 --- a/include/nlohmann/json_view.hpp +++ b/include/nlohmann/json_view.hpp @@ -84,6 +84,7 @@ #include #include #include +#include NLOHMANN_JSON_NAMESPACE_BEGIN @@ -93,13 +94,16 @@ NLOHMANN_JSON_NAMESPACE_BEGIN Trivially copyable (two pointers). Valid as long as the document is alive and has not been re-parsed, and as long as a borrowed source text is alive. */ -template +template class basic_json_view { + /// whether this is a view of an editable document + static constexpr bool is_editable = Mode == view_mode::editable; + using node = detail::view::node; using document_data = detail::view::document_data; /// how the index is walked (with edits only for editable documents) - using navigation = detail::view::navigation; + using navigation = detail::view::navigation; public: using value_t = detail::value_t; @@ -120,6 +124,15 @@ class basic_json_view /// an invalid view (type() == value_t::discarded) basic_json_view() noexcept = default; + /// a view of a read-only document as a view of an editable one, so that a + /// function taking an editable view accepts both (a read-only document has + /// no edits; the view reads it as an editable document before its first + /// edit, at the cost of one check per access) + template::value, int> = 0> + basic_json_view(const basic_json_view& other) noexcept // NOLINT(google-explicit-constructor,hicpp-explicit-conversions) + : m_doc(other.m_doc), m_node(other.m_node) + {} + ////////// // type // ////////// @@ -446,7 +459,7 @@ class basic_json_view if (is_structured() && m_node->len != 0) { return unedited() ? basic_json_view(m_doc, detail::view::last_child(*m_doc, m_node) + (is_object() ? 1 : 0)) - : basic_json_view(m_doc, navigation::value(detail::view::last_child(*m_doc, m_node) + (is_object() ? 1 : 0))); + : basic_json_view(m_doc, navigation::value(detail::view::last_child(*m_doc, m_node) + (is_object() ? 1 : 0))); } return front(); } @@ -504,7 +517,7 @@ class basic_json_view /// more than once); 0 if this is not an object size_type count(string_view_t key) const { - return is_object() ? detail::view::count_members(*m_doc, m_node, key.data(), key.size()) : 0; + return is_object() ? detail::view::count_members(*m_doc, m_node, key.data(), key.size()) : 0; } size_type count(const char* key) const @@ -646,7 +659,7 @@ class basic_json_view } else { - detail::view::view_serializer(*m_doc, out, estimate, style).dump(m_node); + detail::view::view_serializer(*m_doc, out, estimate, style).dump(m_node); } return out; } @@ -682,14 +695,14 @@ class basic_json_view } /// a view of an editable document compares with one of a read-only document - template < bool E, typename std::enable_if < E != Editable, int >::type = 0 > - friend bool operator==(const basic_json_view& a, const basic_json_view& b) + template::value, int> = 0> + friend bool operator==(const basic_json_view& a, const basic_json_view& b) { - return detail::view::equal(side(a), detail::view::view_side>(b)); + return detail::view::equal(side(a), detail::view::view_side>(b)); } - template < bool E, typename std::enable_if < E != Editable, int >::type = 0 > - friend bool operator!=(const basic_json_view& a, const basic_json_view& b) + template::value, int> = 0> + friend bool operator!=(const basic_json_view& a, const basic_json_view& b) { return !(a == b); } @@ -727,7 +740,7 @@ class basic_json_view { return BasicJsonType(value_t::discarded); } - return unedited() ? detail::view::materialize(*m_doc, m_node) : detail::view::materialize(*m_doc, m_node); + return unedited() ? detail::view::materialize(*m_doc, m_node) : detail::view::materialize(*m_doc, m_node); } /// byte offset of this value in the source text (for strings: of the @@ -740,8 +753,8 @@ class basic_json_view } private: - template friend class basic_json_document; - template friend class basic_json_view; + template friend class basic_json_document; + template friend class basic_json_view; template friend class detail::view::editor; friend iterator; @@ -757,11 +770,11 @@ class basic_json_view } /// the template argument as a runtime condition: testing it directly in - /// `Editable && ...` makes the condition constant, which MSVC reports as + /// `is_editable && ...` makes the condition constant, which MSVC reports as /// C4127 static bool editable() noexcept { - return Editable; + return is_editable; } /// the number of source bytes of this value (estimated for values with @@ -810,7 +823,7 @@ class basic_json_view const node* const k = detail::view::find_member(*m_doc, m_node, key.data(), key.size()); return k != nullptr ? basic_json_view(m_doc, k + 1) : basic_json_view(); } - const node* const k = detail::view::find_member(*m_doc, m_node, key.data(), key.size()); + const node* const k = detail::view::find_member(*m_doc, m_node, key.data(), key.size()); return k != nullptr ? basic_json_view(m_doc, navigation::value(k + 1)) : basic_json_view(); } @@ -824,7 +837,7 @@ class basic_json_view /// the member of this object with @a key (the last one if the key occurs more than once), or nullptr NLOHMANN_VIEW_ALWAYS_INLINE const node* find_key(string_view_t key) const { - return find_key(key, std::integral_constant {}); + return find_key(key, std::integral_constant {}); } NLOHMANN_VIEW_ALWAYS_INLINE const node* find_key(string_view_t key, std::true_type /*editable*/) const @@ -846,7 +859,7 @@ class basic_json_view { return basic_json_view(m_doc, detail::view::element_at(*m_doc, m_node, idx)); } - return basic_json_view(m_doc, navigation::value(detail::view::element_at(*m_doc, m_node, idx))); + return basic_json_view(m_doc, navigation::value(detail::view::element_at(*m_doc, m_node, idx))); } // --- get() dispatch --- @@ -952,16 +965,19 @@ Borrowed parses keep a pointer to the caller's text, which must outlive the document. Owned parses (parse_copy, rvalue std::string, streams, and inputs that are not contiguous byte ranges) keep their own copy. */ -template +template class basic_json_document { + /// whether the document can be edited + static constexpr bool is_editable = Mode == view_mode::editable; + using document_data = detail::view::document_data; static_assert(sizeof(typename BasicJsonType::number_integer_t) == 8 && sizeof(typename BasicJsonType::number_unsigned_t) == 8, "json_view supports 64-bit integer types only"); public: - using view_type = basic_json_view; + using view_type = basic_json_view; using value_t = detail::value_t; /// an empty (discarded) document @@ -1357,7 +1373,7 @@ class basic_json_document detail::view::editor editor() { - static_assert(Editable, "only an editable document can be edited: use basic_json_document (json_editable_document)"); + static_assert(is_editable, "only an editable document can be edited: use basic_json_document (json_editable_document)"); if (NLOHMANN_VIEW_UNLIKELY(!m_data || m_data->discarded)) { detail::view::throw_invalid_iterator(detail::exception_id::iterator_from_other_value, "view does not belong to this document"); @@ -1634,21 +1650,21 @@ class basic_json_document }; /// a parsed JSON text for json -using json_document = basic_json_document; +using json_document = basic_json_document; /// a value of a json_document -using json_view = basic_json_view; +using json_view = basic_json_view; /// a parsed JSON text for ordered_json -using ordered_json_document = basic_json_document; +using ordered_json_document = basic_json_document; /// a value of an ordered_json_document -using ordered_json_view = basic_json_view; +using ordered_json_view = basic_json_view; /// an editable parsed JSON text for json -using json_editable_document = basic_json_document; +using json_editable_document = basic_json_document; /// a value of a json_editable_document -using json_editable_view = basic_json_view; +using json_editable_view = basic_json_view; /// an editable parsed JSON text for ordered_json -using ordered_json_editable_document = basic_json_document; +using ordered_json_editable_document = basic_json_document; /// a value of an ordered_json_editable_document -using ordered_json_editable_view = basic_json_view; +using ordered_json_editable_view = basic_json_view; NLOHMANN_JSON_NAMESPACE_END diff --git a/single_include/nlohmann/json_view.hpp b/single_include/nlohmann/json_view.hpp index 338b55492..1fee76e63 100644 --- a/single_include/nlohmann/json_view.hpp +++ b/single_include/nlohmann/json_view.hpp @@ -239,9 +239,22 @@ struct node_flags static constexpr std::uint8_t linked = 32; ///< an entry of a moved sequence links to this value (editable documents): its extent in the parsed layout no longer matters }; -/// kind of an entry of an edited sequence that stands for a value stored -/// elsewhere (the address of the value's node is kept in the len/next bytes) -constexpr std::uint8_t kind_link = 10; +/// the kind of a node: the value_t of its value, or link +enum class node_kind : std::uint8_t +{ + null = static_cast(value_t::null), + object = static_cast(value_t::object), + array = static_cast(value_t::array), + string = static_cast(value_t::string), // NOLINT(build/include_what_you_use): an enumerator, not std::string + boolean = static_cast(value_t::boolean), + number_integer = static_cast(value_t::number_integer), + number_unsigned = static_cast(value_t::number_unsigned), + number_float = static_cast(value_t::number_float), + /// an entry of an edited sequence that stands for a value stored elsewhere + /// (the address of the value's node is kept in the len/next bytes); placed + /// after value_t::discarded, which no node ever has + link = static_cast(value_t::discarded) + 1, +}; /// One entry of the flat index, in document order. An object's members are /// stored as key node followed by the value's subtree. Integers keep their @@ -249,7 +262,7 @@ constexpr std::uint8_t kind_link = 10; /// always the next one, and the token length follows from `extra`). struct node { - std::uint8_t kind; ///< value_t, or kind_link + node_kind kind; ///< the kind of the value, or link std::uint8_t flags; ///< node_flags std::uint16_t extra; ///< numbers: integer digits (low byte) and fraction digits (high byte), 255 = "many"; objects: number of the object's key hash index (1-based, 0 = none); otherwise 0 std::uint32_t off; ///< source offset (string content, number token, literal, bracket); arena offset if escaped/edited; number of the element sequence if moved @@ -258,13 +271,13 @@ struct node }; static_assert(sizeof(node) == 16, "node must stay 16 bytes"); -// the node kinds are value_t values; the test below (object and array are 1 -// and 2) and those of the number kinds depend on this numbering -static_assert(static_cast(value_t::null) == 0 && static_cast(value_t::object) == 1 - && static_cast(value_t::array) == 2 && static_cast(value_t::string) == 3 - && static_cast(value_t::boolean) == 4 && static_cast(value_t::number_integer) == 5 - && static_cast(value_t::number_unsigned) == 6 && static_cast(value_t::number_float) == 7, - "the node format depends on the numbering of value_t"); +// images store the node kinds as bytes, and the test below (object and array +// are 1 and 2) and those of the number kinds depend on this numbering +static_assert(static_cast(node_kind::null) == 0 && static_cast(node_kind::object) == 1 + && static_cast(node_kind::array) == 2 && static_cast(node_kind::string) == 3 + && static_cast(node_kind::boolean) == 4 && static_cast(node_kind::number_integer) == 5 + && static_cast(node_kind::number_unsigned) == 6 && static_cast(node_kind::number_float) == 7, + "the node format depends on the numbering of node_kind"); NLOHMANN_VIEW_ALWAYS_INLINE bool is_container(const node& n) noexcept { @@ -284,7 +297,7 @@ inline void make_link(node& n, node* target) noexcept { target->flags = static_cast(target->flags | node_flags::linked); n = node{}; - n.kind = kind_link; + n.kind = node_kind::link; std::memcpy(reinterpret_cast(&n) + 8, static_cast(&target), sizeof(const node*)); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) } @@ -314,8 +327,8 @@ NLOHMANN_VIEW_ALWAYS_INLINE void set_integer_bits(node& n, std::uint64_t v) noex /// token length of a number node NLOHMANN_VIEW_ALWAYS_INLINE std::uint32_t number_length(const node& n) noexcept { - return n.kind == static_cast(value_t::number_float) ? n.len - : (n.extra & 0xFFu) + (n.kind == static_cast(value_t::number_integer) ? 1u : 0u); + return n.kind == node_kind::number_float ? n.len + : (n.extra & 0xFFu) + (n.kind == node_kind::number_integer ? 1u : 0u); } /// estimated number of nodes for an input of `size` bytes (one node per ~12 @@ -547,7 +560,7 @@ struct document_data /// following loads. static NLOHMANN_VIEW_ALWAYS_INLINE const node* deref(const node* n) noexcept { - return NLOHMANN_VIEW_LIKELY(n->kind != kind_link) ? n : follow_link(n); + return NLOHMANN_VIEW_LIKELY(n->kind != node_kind::link) ? n : follow_link(n); } static NLOHMANN_VIEW_NOINLINE const node* follow_link(const node* n) noexcept @@ -1830,11 +1843,11 @@ run_found: switch (cur()) { case '{': - open(value_t::object); + open(node_kind::object); ++p; goto obj_first; case '[': - open(value_t::array); + open(node_kind::array); ++p; goto arr_first; default: @@ -1856,11 +1869,11 @@ run_found: if (NLOHMANN_VIEW_UNLIKELY(!string())) { return false; } \ break; \ case '{': \ - open(value_t::object); \ + open(node_kind::object); \ ++p; \ goto obj_first; \ case '[': \ - open(value_t::array); \ + open(node_kind::array); \ ++p; \ goto arr_first; \ case '-': \ @@ -2077,17 +2090,17 @@ root_done: NLOHMANN_VIEW_ALWAYS_INLINE bool literal_true() { - return literal("true", value_t::boolean, node_flags::is_true); + return literal("true", node_kind::boolean, node_flags::is_true); } NLOHMANN_VIEW_ALWAYS_INLINE bool literal_false() { - return literal("false", value_t::boolean, 0); + return literal("false", node_kind::boolean, 0); } NLOHMANN_VIEW_ALWAYS_INLINE bool literal_null() { - return literal("null", value_t::null, 0); + return literal("null", node_kind::null, 0); } /// skip whitespace (and comments); false on a malformed comment @@ -2153,7 +2166,7 @@ indent_done: /// append a node: (kind, flags, extra, off) and the second word (len, or /// an integer's value); two stores on little-endian targets - NLOHMANN_VIEW_ALWAYS_INLINE node* push_node(value_t k, std::uint8_t flags, std::uint16_t extra, std::size_t off, std::uint64_t second) + NLOHMANN_VIEW_ALWAYS_INLINE node* push_node(node_kind k, std::uint8_t flags, std::uint16_t extra, std::size_t off, std::uint64_t second) { if (NLOHMANN_VIEW_UNLIKELY(out == cap)) { @@ -2169,7 +2182,7 @@ indent_done: std::memcpy(reinterpret_cast(n), &first, 8); std::memcpy(reinterpret_cast(n) + 8, &second, 8); #else - n->kind = static_cast(k); + n->kind = k; n->flags = flags; n->extra = extra; n->off = static_cast(off); @@ -2181,7 +2194,7 @@ indent_done: return n; } - NLOHMANN_VIEW_ALWAYS_INLINE void open(value_t k) + NLOHMANN_VIEW_ALWAYS_INLINE void open(node_kind k) { // (base is read after push_node(), which moves the node array when it // grows; in one expression the order of the two is unspecified) @@ -2208,7 +2221,7 @@ indent_done: max_depth = (std::max)(max_depth, depth); cur_idx = idx; cur_count = 0; - cur_is_object = k == value_t::object; + cur_is_object = k == node_kind::object; } NLOHMANN_VIEW_ALWAYS_INLINE void close() @@ -2238,7 +2251,7 @@ indent_done: /// the literal `text` at p (N counts the terminating NUL) template - NLOHMANN_VIEW_ALWAYS_INLINE bool literal(const char (&text)[N], value_t k, std::uint8_t flags) // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays): a string literal + NLOHMANN_VIEW_ALWAYS_INLINE bool literal(const char (&text)[N], node_kind k, std::uint8_t flags) // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays): a string literal { const std::size_t n = N - 1; if (NLOHMANN_VIEW_UNLIKELY(e - p < static_cast(n) || std::memcmp(p, text, n) != 0)) @@ -2317,23 +2330,23 @@ indent_done: is_float = true; } - value_t kind = value_t::number_float; + node_kind kind = node_kind::number_float; if (!is_float) { // integers that do not fit become floats, as in parse() if (negative) { - kind = digits_exceed(int_start, int_digits, "9223372036854775808") ? value_t::number_float : value_t::number_integer; + kind = digits_exceed(int_start, int_digits, "9223372036854775808") ? node_kind::number_float : node_kind::number_integer; } else { - kind = digits_exceed(int_start, int_digits, "18446744073709551615") ? value_t::number_float : value_t::number_unsigned; + kind = digits_exceed(int_start, int_digits, "18446744073709551615") ? node_kind::number_float : node_kind::number_unsigned; } } // parse() rejects floats that overflow; only numbers whose magnitude // could reach the largest FloatType (1e308 for double, 1e38 for // float) need the conversion - if (NLOHMANN_VIEW_UNLIKELY(static_cast(int_digits) + exponent > std::numeric_limits::max_exponent10 - 8 && kind == value_t::number_float)) + if (NLOHMANN_VIEW_UNLIKELY(static_cast(int_digits) + exponent > std::numeric_limits::max_exponent10 - 8 && kind == node_kind::number_float)) { if (builder::float_overflows(s, p)) { @@ -2343,7 +2356,7 @@ indent_done: } const auto layout = static_cast((int_digits < 255 ? int_digits : 255) | ((frac_digits < 255 ? frac_digits : 255) << 8)); auto second = static_cast(p - s); - if (kind != value_t::number_float) + if (kind != node_kind::number_float) { // integers are converted now, while their digits are in cache const std::uint64_t m = int_digits <= 19 ? parse_upto19(int_start, static_cast(int_digits), e) @@ -2366,7 +2379,7 @@ indent_done: p = scan_string_run(p, e); if (NLOHMANN_VIEW_LIKELY(cur() == '"')) { - push_node(value_t::string, 0, 0, static_cast(s - b), static_cast(p - s)); + push_node(node_kind::string, 0, 0, static_cast(s - b), static_cast(p - s)); ++p; return true; } @@ -2376,7 +2389,7 @@ indent_done: return false; } p = r.p; - push_node(value_t::string, node_flags::escaped, 0, r.start, r.len); + push_node(node_kind::string, node_flags::escaped, 0, r.start, r.len); return true; } }; @@ -3001,7 +3014,7 @@ NLOHMANN_JSON_NAMESPACE_END // live in chunks that never move; strings and number tokens written by edits // live in the edit arena. An array/object whose elements change gets // node_flags::moved: its elements then live in a separate sequence (a header -// node, then the entries), whose entries link to the values (kind_link). +// node, then the entries), whose entries link to the values (node_kind::link). NLOHMANN_JSON_NAMESPACE_BEGIN namespace detail @@ -3159,7 +3172,7 @@ inline node* block_of(document_data& d, node* n, std::size_t extra) return nh; } reserve_moved(d); // (so that set_moved() below cannot throw: the links are marked before) - const bool object = n->kind == static_cast(value_t::object); + const bool object = n->kind == node_kind::object; const std::size_t used = 1 + (static_cast(n->len) * (object ? 2 : 1)); const std::size_t cap = used + extra; node* const h = alloc_nodes(d, cap); @@ -3225,7 +3238,7 @@ inline node* find_parent(const document_data& d, const node* target) { return nullptr; // LCOV_EXCL_LINE (the value is inside c) } - const bool object = c->kind == static_cast(value_t::object); + const bool object = c->kind == node_kind::object; const node* down = nullptr; for (const node* p = d.first_child_edited(c), *e = d.child_end_edited(c); p != e;) { @@ -3692,7 +3705,7 @@ const node* element_at(const document_data& d, const node* array, std::size_t id template const node* last_child(const document_data& d, const node* container) noexcept { - const std::size_t value_offset = container->kind == static_cast(value_t::object) ? 1 : 0; + const std::size_t value_offset = container->kind == node_kind::object ? 1 : 0; const node* const end = navigation::end(d, container); const node* last = navigation::first(d, container); if (value_offset == 0 && container->next == container->len + 1u && (!navigation::editable() || (container->flags & node_flags::moved) == 0)) @@ -3714,10 +3727,32 @@ NLOHMANN_JSON_NAMESPACE_END // #include +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +#include // uint8_t +#include // false_type, true_type + +// #include NLOHMANN_JSON_NAMESPACE_BEGIN -template +/// whether a basic_json_document and its views can be edited +enum class view_mode : std::uint8_t +{ + read_only, ///< parsed once, then only read (the default) + editable, ///< also has set(), push_back(), insert(), and erase() +}; + +template class basic_json_view; namespace detail @@ -3725,6 +3760,35 @@ namespace detail namespace view { +/// whether two view modes differ +template +struct is_other_mode : std::true_type {}; + +template +struct is_other_mode : std::false_type {}; + +/// whether a view of mode From converts implicitly to one of mode To: a +/// read-only document has no edits, so a view of it is read correctly as a +/// view of an editable document that has not been edited yet +template +struct is_widening_mode : std::false_type {}; + +template<> +struct is_widening_mode : std::true_type {}; + +} // namespace view +} // namespace detail + +NLOHMANN_JSON_NAMESPACE_END + + +NLOHMANN_JSON_NAMESPACE_BEGIN + +namespace detail +{ +namespace view +{ + /// the index of the first true condition (the number of conditions if none is) template struct first_true : std::integral_constant {}; @@ -3808,15 +3872,15 @@ class editor View set(const View& object, string_view_t key, V&& value) { node* const o = own(object); - if (o->kind != static_cast(value_t::object) && o->kind != static_cast(value_t::null)) + if (o->kind != node_kind::object && o->kind != node_kind::null) { throw_type_error(exception_id::subscript_wrong_type, "cannot use operator[] with a string argument with ", object.type_name()); } check_utf8(key.data(), key.size()); const encoded e = encode(std::forward(value)); - if (o->kind == static_cast(value_t::null)) + if (o->kind == node_kind::null) { - become_empty(o, value_t::object); + become_empty(o, node_kind::object); } // an existing member: assign it (and drop later duplicates, so that // lookups, iteration, and materialize() agree) @@ -3859,7 +3923,7 @@ class editor View set(const View& array, std::size_t idx, V&& value) { node* const a = own(array); - if (a->kind != static_cast(value_t::array)) + if (a->kind != node_kind::array) { throw_type_error(exception_id::subscript_wrong_type, "cannot use operator[] with a numeric argument with ", array.type_name()); } @@ -3876,14 +3940,14 @@ class editor View push_back(const View& array, V&& value) { node* const a = own(array); - if (a->kind != static_cast(value_t::array) && a->kind != static_cast(value_t::null)) + if (a->kind != node_kind::array && a->kind != node_kind::null) { throw_type_error(exception_id::push_back_wrong_type, "cannot use push_back() with ", array.type_name()); } const encoded e = encode(std::forward(value)); - if (a->kind == static_cast(value_t::null)) + if (a->kind == node_kind::null) { - become_empty(a, value_t::array); + become_empty(a, node_kind::array); } node* const slot = new_slot(e); node* const h = block_of(m_doc, a, 1); @@ -3900,7 +3964,7 @@ class editor View insert(const View& array, std::size_t idx, V&& value) { node* const a = own(array); - if (a->kind != static_cast(value_t::array)) + if (a->kind != node_kind::array) { throw_type_error(exception_id::insert_wrong_type, "cannot use insert() with ", array.type_name()); } @@ -3920,7 +3984,7 @@ class editor std::size_t erase(const View& object, string_view_t key) { node* const o = own(object); - if (o->kind != static_cast(value_t::object)) + if (o->kind != node_kind::object) { throw_type_error(exception_id::erase_wrong_type, "cannot use erase() with ", object.type_name()); } @@ -3938,7 +4002,7 @@ class editor void erase(const View& array, std::size_t idx) { node* const a = own(array); - if (a->kind != static_cast(value_t::array)) + if (a->kind != node_kind::array) { throw_type_error(exception_id::erase_wrong_type, "cannot use erase() with ", array.type_name()); } @@ -4012,11 +4076,11 @@ class editor } /// turn a null into an empty array/object in place - static void become_empty(node* n, value_t k) noexcept + static void become_empty(node* n, node_kind k) noexcept { const std::uint8_t linked = n->flags & node_flags::linked; *n = node{}; - n->kind = static_cast(k); + n->kind = k; n->flags = static_cast(node_flags::is_new | linked); n->next = 1; } @@ -4093,8 +4157,8 @@ class editor template struct is_view : std::false_type {}; - template - struct is_view> : std::true_type {}; + template + struct is_view> : std::true_type {}; template encoded encode(V&& v) @@ -4109,8 +4173,8 @@ class editor } /// a view of any document (copied; nothing is shared with it) - template - encoded encode_impl(const basic_json_view& v, encode_tag<0> /*view*/) + template + encoded encode_impl(const basic_json_view& v, encode_tag<0> /*view*/) { if (NLOHMANN_VIEW_UNLIKELY(v.m_node == nullptr)) { @@ -4122,8 +4186,8 @@ class editor r.scalar = copy_scalar(*v.m_doc, *v.m_node); return r; } - r.region = alloc_nodes(m_doc, count_nodes(*v.m_doc, v.m_node)); - fill_nodes(*v.m_doc, v.m_node, r.region); + r.region = alloc_nodes(m_doc, count_nodes(*v.m_doc, v.m_node)); + fill_nodes(*v.m_doc, v.m_node, r.region); edit_state_of(m_doc).regions.emplace(r.region, nullptr); return r; } @@ -4145,14 +4209,14 @@ class editor encoded encode_impl(std::nullptr_t /*unused*/, encode_tag<2> /*null*/) { encoded r; - r.scalar = plain_node(value_t::null); + r.scalar = plain_node(node_kind::null); return r; } encoded encode_impl(bool b, encode_tag<3> /*boolean*/) { encoded r; - r.scalar = plain_node(value_t::boolean); + r.scalar = plain_node(node_kind::boolean); r.scalar.flags = static_cast(r.scalar.flags | (b ? node_flags::is_true : 0)); return r; } @@ -4181,10 +4245,10 @@ class editor return encode_impl(BasicJsonType(std::forward(x)), encode_tag<1> {}); } - static node plain_node(value_t k) noexcept + static node plain_node(node_kind k) noexcept { node n{}; - n.kind = static_cast(k); + n.kind = k; n.flags = node_flags::is_new; return n; } @@ -4198,19 +4262,19 @@ class editor template node number_node(T x, std::integral_constant /*signed*/) { - return integer_node(static_cast(static_cast(x)), value_t::number_integer); + return integer_node(static_cast(static_cast(x)), node_kind::number_integer); } template node number_node(T x, std::integral_constant /*unsigned*/) { - return integer_node(static_cast(x), value_t::number_unsigned); + return integer_node(static_cast(x), node_kind::number_unsigned); } /// an integer with its canonical token in the edit arena - node integer_node(std::uint64_t bits, value_t k) + node integer_node(std::uint64_t bits, node_kind k) { - const bool negative = k == value_t::number_integer && static_cast(bits) < 0; + const bool negative = k == node_kind::number_integer && static_cast(bits) < 0; std::uint64_t magnitude = negative ? 0 - bits : bits; std::array buf{}; char* p = buf.data() + buf.size(); // NOLINT(misc-const-correctness): digits are written through p @@ -4229,7 +4293,7 @@ class editor n.flags = static_cast(n.flags | node_flags::edited); n.off = append_text(m_doc, p, len); // number_length() adds one for the sign of number_integer nodes - n.extra = static_cast(k == value_t::number_integer ? len - 1 : len); + n.extra = static_cast(k == node_kind::number_integer ? len - 1 : len); set_integer_bits(n, bits); return n; } @@ -4251,7 +4315,7 @@ class editor { text = BasicJsonType(x).dump(); } - node n = plain_node(value_t::number_float); + node n = plain_node(node_kind::number_float); n.flags = static_cast(n.flags | node_flags::edited); n.extra = 0xFFFFu; // (the digit layout is not recorded) n.off = append_text(m_doc, text.data(), text.size()); @@ -4266,7 +4330,7 @@ class editor { throw_out_of_range(exception_id::document_too_large, "strings of 4 GiB or more are not supported by json_document"); // LCOV_EXCL_LINE } - node n = plain_node(value_t::string); + node n = plain_node(node_kind::string); n.flags = static_cast(n.flags | node_flags::edited); n.off = append_text(m_doc, s, len); n.len = static_cast(len); @@ -4288,10 +4352,10 @@ class editor return string_node(from.str(n), n.len); case value_t::number_integer: case value_t::number_unsigned: - return integer_node(integer_bits(n), static_cast(n.kind)); + return integer_node(integer_bits(n), n.kind); case value_t::number_float: { - node r = plain_node(value_t::number_float); + node r = plain_node(node_kind::number_float); r.flags = static_cast(r.flags | node_flags::edited); r.off = append_text(m_doc, from.str(n), n.len); r.len = n.len; @@ -4300,7 +4364,7 @@ class editor } case value_t::boolean: { - node r = plain_node(value_t::boolean); + node r = plain_node(node_kind::boolean); r.flags = static_cast(r.flags | (n.flags & node_flags::is_true)); return r; } @@ -4310,7 +4374,7 @@ class editor case value_t::binary: case value_t::discarded: default: - return plain_node(value_t::null); + return plain_node(node_kind::null); } } @@ -4319,17 +4383,17 @@ class editor switch (j.type()) { case value_t::null: - return plain_node(value_t::null); + return plain_node(node_kind::null); case value_t::boolean: { - node r = plain_node(value_t::boolean); + node r = plain_node(node_kind::boolean); r.flags = static_cast(r.flags | (j.template get() ? node_flags::is_true : 0)); return r; } case value_t::number_integer: - return integer_node(static_cast(static_cast(j.template get())), value_t::number_integer); + return integer_node(static_cast(static_cast(j.template get())), node_kind::number_integer); case value_t::number_unsigned: - return integer_node(static_cast(j.template get()), value_t::number_unsigned); + return integer_node(static_cast(j.template get()), node_kind::number_unsigned); case value_t::number_float: return float_node(j.template get()); case value_t::string: @@ -4369,7 +4433,7 @@ class editor ++r; if (is_container(*n)) { - open.push_back(frame{walk::first(d, n), walk::end(d, n), n->kind == static_cast(value_t::object)}); + open.push_back(frame{walk::first(d, n), walk::end(d, n), n->kind == node_kind::object}); } // the next value: close finished containers, then step over the key for (;;) @@ -4416,9 +4480,9 @@ class editor if (is_container(*n)) { node* const self = out++; - *self = plain_node(static_cast(n->kind)); + *self = plain_node(n->kind); self->len = n->len; - open.push_back(frame{walk::first(d, n), walk::end(d, n), n->kind == static_cast(value_t::object), self}); + open.push_back(frame{walk::first(d, n), walk::end(d, n), n->kind == node_kind::object, self}); } else { @@ -4507,7 +4571,7 @@ class editor if (n->is_structured()) { node* const self = out++; - *self = plain_node(n->type()); + *self = plain_node(static_cast(n->type())); self->len = static_cast(n->size()); open.push_back(frame{n->cbegin(), n->cend(), n->is_object(), self}); } @@ -4903,7 +4967,7 @@ inline void compact_nodes(const document_data& d, std::size_t arena_size, std::v r.off = (v->flags & (node_flags::moved | node_flags::is_new)) != 0 ? 0 : v->off; r.len = 0; // counted below r.next = 0; // set when the container is complete - stack.push_back(frame{d.first_child_edited(v), d.child_end_edited(v), out.size(), 0, v->kind == static_cast(value_t::object)}); + stack.push_back(frame{d.first_child_edited(v), d.child_end_edited(v), out.size(), 0, v->kind == node_kind::object}); break; case value_t::string: r = string_node(*v); @@ -4924,7 +4988,7 @@ inline void compact_nodes(const document_data& d, std::size_t arena_size, std::v if (t[0] == 'n' || t[0] == 'i' || (v->len > 1 && t[1] == 'i')) { r = node{}; // nan and infinity: null, as dump() writes them - r.kind = static_cast(value_t::null); + r.kind = node_kind::null; break; } r.off = static_cast(d.size + text_tail.size()); @@ -5007,7 +5071,7 @@ inline std::vector save_image(const document_data& d) // extra of an object, and the same document saves to the same bytes for (std::size_t i = 0; i < count; ++i) { - if (nodes[i].kind == static_cast(value_t::object) && nodes[i].extra != 0) + if (nodes[i].kind == node_kind::object && nodes[i].extra != 0) { std::memset(o + (i * sizeof(node)) + offsetof(node, extra), 0, sizeof(nodes[i].extra)); } @@ -5123,7 +5187,7 @@ inline bool float_token_finite(const unsigned char* s, std::size_t len, const nu if (static_cast(t.int_digits) + t.exponent > 300) { node n{}; - n.kind = static_cast(value_t::number_float); + n.kind = node_kind::number_float; n.len = static_cast(len); const auto v = float_value(reinterpret_cast(s), n); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) return v <= (std::numeric_limits::max)() && v >= -(std::numeric_limits::max)(); @@ -5144,7 +5208,7 @@ inline bool check_integer(const node& n, const unsigned char* text) } // the token's value is the stored one; number_integer nodes of edits can // be non-negative (as basic_json keeps the type of a value) - const bool integer = n.kind == static_cast(value_t::number_integer); + const bool integer = n.kind == node_kind::number_integer; if (t.is_float || t.int_digits > 20 || (t.negative && !integer)) { return false; @@ -5326,7 +5390,7 @@ inline bool check_image(const node* nodes, std::size_t count, const unsigned cha { return false; } - if (n.kind != static_cast(value_t::number_float)) + if (n.kind != node_kind::number_float) { return (n.extra >> 8u) == 0; } @@ -5367,7 +5431,7 @@ inline bool check_image(const node* nodes, std::size_t count, const unsigned cha const node& n = nodes[i]; if (!stack.empty() && stack.back().object && stack.back().expect_key) { - if (n.kind != static_cast(value_t::string) || !check_string(n)) + if (n.kind != node_kind::string || !check_string(n)) { return false; } @@ -5408,7 +5472,7 @@ inline bool check_image(const node* nodes, std::size_t count, const unsigned cha { // (the contents of float tokens are checked later; the token of an // integer has at most 256 characters) - if (n.kind == static_cast(value_t::number_float)) + if (n.kind == node_kind::number_float) { floats.push_back(float_range{n.off, n.len, n.extra}); } @@ -5426,7 +5490,7 @@ inline bool check_image(const node* nodes, std::size_t count, const unsigned cha { return false; } - stack.push_back(frame{i + n.next, n.len, 0, n.kind == static_cast(value_t::object), true}); + stack.push_back(frame{i + n.next, n.len, 0, n.kind == node_kind::object, true}); complete = false; break; } @@ -5515,7 +5579,7 @@ inline void load_image(document_data& d, const std::uint8_t* image, std::size_t for (std::size_t i = 0; i < count; ++i) { const node& n = d.nodes[i]; - if (n.kind == static_cast(value_t::object) && n.len >= document_data::index_min_members) + if (n.kind == node_kind::object && n.len >= document_data::index_min_members) { d.large_objects.push_back(static_cast(i)); } @@ -6108,7 +6172,7 @@ BasicJsonType materialize(const document_data& d, const node* n) case value_t::object: case value_t::array: { - const bool object = n->kind == static_cast(value_t::object); + const bool object = n->kind == node_kind::object; if (object) { sax.start_object(n->len); @@ -7222,7 +7286,7 @@ class view_serializer case value_t::object: case value_t::array: { - const bool object = n->kind == static_cast(value_t::object); + const bool object = n->kind == node_kind::object; room(8); if (n->len == 0) { @@ -7773,6 +7837,8 @@ struct is_direct_value, BasicJsonType, View> : } // namespace detail NLOHMANN_JSON_NAMESPACE_END +// #include + NLOHMANN_JSON_NAMESPACE_BEGIN @@ -7782,13 +7848,16 @@ NLOHMANN_JSON_NAMESPACE_BEGIN Trivially copyable (two pointers). Valid as long as the document is alive and has not been re-parsed, and as long as a borrowed source text is alive. */ -template +template class basic_json_view { + /// whether this is a view of an editable document + static constexpr bool is_editable = Mode == view_mode::editable; + using node = detail::view::node; using document_data = detail::view::document_data; /// how the index is walked (with edits only for editable documents) - using navigation = detail::view::navigation; + using navigation = detail::view::navigation; public: using value_t = detail::value_t; @@ -7809,6 +7878,15 @@ class basic_json_view /// an invalid view (type() == value_t::discarded) basic_json_view() noexcept = default; + /// a view of a read-only document as a view of an editable one, so that a + /// function taking an editable view accepts both (a read-only document has + /// no edits; the view reads it as an editable document before its first + /// edit, at the cost of one check per access) + template::value, int> = 0> + basic_json_view(const basic_json_view& other) noexcept // NOLINT(google-explicit-constructor,hicpp-explicit-conversions) + : m_doc(other.m_doc), m_node(other.m_node) + {} + ////////// // type // ////////// @@ -8135,7 +8213,7 @@ class basic_json_view if (is_structured() && m_node->len != 0) { return unedited() ? basic_json_view(m_doc, detail::view::last_child(*m_doc, m_node) + (is_object() ? 1 : 0)) - : basic_json_view(m_doc, navigation::value(detail::view::last_child(*m_doc, m_node) + (is_object() ? 1 : 0))); + : basic_json_view(m_doc, navigation::value(detail::view::last_child(*m_doc, m_node) + (is_object() ? 1 : 0))); } return front(); } @@ -8193,7 +8271,7 @@ class basic_json_view /// more than once); 0 if this is not an object size_type count(string_view_t key) const { - return is_object() ? detail::view::count_members(*m_doc, m_node, key.data(), key.size()) : 0; + return is_object() ? detail::view::count_members(*m_doc, m_node, key.data(), key.size()) : 0; } size_type count(const char* key) const @@ -8335,7 +8413,7 @@ class basic_json_view } else { - detail::view::view_serializer(*m_doc, out, estimate, style).dump(m_node); + detail::view::view_serializer(*m_doc, out, estimate, style).dump(m_node); } return out; } @@ -8371,14 +8449,14 @@ class basic_json_view } /// a view of an editable document compares with one of a read-only document - template < bool E, typename std::enable_if < E != Editable, int >::type = 0 > - friend bool operator==(const basic_json_view& a, const basic_json_view& b) + template::value, int> = 0> + friend bool operator==(const basic_json_view& a, const basic_json_view& b) { - return detail::view::equal(side(a), detail::view::view_side>(b)); + return detail::view::equal(side(a), detail::view::view_side>(b)); } - template < bool E, typename std::enable_if < E != Editable, int >::type = 0 > - friend bool operator!=(const basic_json_view& a, const basic_json_view& b) + template::value, int> = 0> + friend bool operator!=(const basic_json_view& a, const basic_json_view& b) { return !(a == b); } @@ -8416,7 +8494,7 @@ class basic_json_view { return BasicJsonType(value_t::discarded); } - return unedited() ? detail::view::materialize(*m_doc, m_node) : detail::view::materialize(*m_doc, m_node); + return unedited() ? detail::view::materialize(*m_doc, m_node) : detail::view::materialize(*m_doc, m_node); } /// byte offset of this value in the source text (for strings: of the @@ -8429,8 +8507,8 @@ class basic_json_view } private: - template friend class basic_json_document; - template friend class basic_json_view; + template friend class basic_json_document; + template friend class basic_json_view; template friend class detail::view::editor; friend iterator; @@ -8446,11 +8524,11 @@ class basic_json_view } /// the template argument as a runtime condition: testing it directly in - /// `Editable && ...` makes the condition constant, which MSVC reports as + /// `is_editable && ...` makes the condition constant, which MSVC reports as /// C4127 static bool editable() noexcept { - return Editable; + return is_editable; } /// the number of source bytes of this value (estimated for values with @@ -8499,7 +8577,7 @@ class basic_json_view const node* const k = detail::view::find_member(*m_doc, m_node, key.data(), key.size()); return k != nullptr ? basic_json_view(m_doc, k + 1) : basic_json_view(); } - const node* const k = detail::view::find_member(*m_doc, m_node, key.data(), key.size()); + const node* const k = detail::view::find_member(*m_doc, m_node, key.data(), key.size()); return k != nullptr ? basic_json_view(m_doc, navigation::value(k + 1)) : basic_json_view(); } @@ -8513,7 +8591,7 @@ class basic_json_view /// the member of this object with @a key (the last one if the key occurs more than once), or nullptr NLOHMANN_VIEW_ALWAYS_INLINE const node* find_key(string_view_t key) const { - return find_key(key, std::integral_constant {}); + return find_key(key, std::integral_constant {}); } NLOHMANN_VIEW_ALWAYS_INLINE const node* find_key(string_view_t key, std::true_type /*editable*/) const @@ -8535,7 +8613,7 @@ class basic_json_view { return basic_json_view(m_doc, detail::view::element_at(*m_doc, m_node, idx)); } - return basic_json_view(m_doc, navigation::value(detail::view::element_at(*m_doc, m_node, idx))); + return basic_json_view(m_doc, navigation::value(detail::view::element_at(*m_doc, m_node, idx))); } // --- get() dispatch --- @@ -8641,16 +8719,19 @@ Borrowed parses keep a pointer to the caller's text, which must outlive the document. Owned parses (parse_copy, rvalue std::string, streams, and inputs that are not contiguous byte ranges) keep their own copy. */ -template +template class basic_json_document { + /// whether the document can be edited + static constexpr bool is_editable = Mode == view_mode::editable; + using document_data = detail::view::document_data; static_assert(sizeof(typename BasicJsonType::number_integer_t) == 8 && sizeof(typename BasicJsonType::number_unsigned_t) == 8, "json_view supports 64-bit integer types only"); public: - using view_type = basic_json_view; + using view_type = basic_json_view; using value_t = detail::value_t; /// an empty (discarded) document @@ -9046,7 +9127,7 @@ class basic_json_document detail::view::editor editor() { - static_assert(Editable, "only an editable document can be edited: use basic_json_document (json_editable_document)"); + static_assert(is_editable, "only an editable document can be edited: use basic_json_document (json_editable_document)"); if (NLOHMANN_VIEW_UNLIKELY(!m_data || m_data->discarded)) { detail::view::throw_invalid_iterator(detail::exception_id::iterator_from_other_value, "view does not belong to this document"); @@ -9323,21 +9404,21 @@ class basic_json_document }; /// a parsed JSON text for json -using json_document = basic_json_document; +using json_document = basic_json_document; /// a value of a json_document -using json_view = basic_json_view; +using json_view = basic_json_view; /// a parsed JSON text for ordered_json -using ordered_json_document = basic_json_document; +using ordered_json_document = basic_json_document; /// a value of an ordered_json_document -using ordered_json_view = basic_json_view; +using ordered_json_view = basic_json_view; /// an editable parsed JSON text for json -using json_editable_document = basic_json_document; +using json_editable_document = basic_json_document; /// a value of a json_editable_document -using json_editable_view = basic_json_view; +using json_editable_view = basic_json_view; /// an editable parsed JSON text for ordered_json -using ordered_json_editable_document = basic_json_document; +using ordered_json_editable_document = basic_json_document; /// a value of an ordered_json_editable_document -using ordered_json_editable_view = basic_json_view; +using ordered_json_editable_view = basic_json_view; NLOHMANN_JSON_NAMESPACE_END diff --git a/tests/src/unit-json_view_builder.cpp b/tests/src/unit-json_view_builder.cpp index 6935981fb..1074f7018 100644 --- a/tests/src/unit-json_view_builder.cpp +++ b/tests/src/unit-json_view_builder.cpp @@ -592,7 +592,7 @@ TEST_CASE("json_view node integer bits") }) { CAPTURE(v) - n.kind = 0x5A; + n.kind = static_cast(0x5A); n.flags = 0xA5; n.extra = 0x1234; n.off = 0x89ABCDEFu; @@ -601,7 +601,7 @@ TEST_CASE("json_view node integer bits") CHECK(n.len == static_cast(v)); CHECK(n.next == static_cast(v >> 32)); // the other fields are untouched - CHECK(n.kind == 0x5A); + CHECK(static_cast(n.kind) == 0x5A); CHECK(n.flags == 0xA5); CHECK(n.extra == 0x1234); CHECK(n.off == 0x89ABCDEFu); diff --git a/tests/src/unit-json_view_edit_values.cpp b/tests/src/unit-json_view_edit_values.cpp index 59aee3a9d..39db8d0ba 100644 --- a/tests/src/unit-json_view_edit_values.cpp +++ b/tests/src/unit-json_view_edit_values.cpp @@ -26,12 +26,119 @@ using nlohmann::json_editable_view; #include #include #include +#include #include #include // These tests were split off unit-json_view_edit.cpp, whose object file got too // large for the MinGW linker (see json_view_test_helpers.hpp). +namespace +{ +// the three ways a function can accept views: editable ones, read-only ones, or both +template +int accepts_editable(nlohmann::basic_json_view /*unused*/) +{ + return 1; +} + +template +int accepts_read_only(nlohmann::basic_json_view /*unused*/) +{ + return 2; +} + +template +int accepts_any(nlohmann::basic_json_view /*unused*/) +{ + return Mode == nlohmann::view_mode::editable ? 3 : 4; +} + +template +auto overload(View v) -> decltype(accepts_editable(v)) +{ + return accepts_editable(v); +} + +template +auto overload(View v) -> decltype(accepts_read_only(v)) +{ + return accepts_read_only(v); +} + +// a function (no template) that takes an editable view also accepts a read-only one +std::size_t sum_of(json_editable_view v) +{ + std::size_t sum = 0; + for (const json_editable_view x : v.at("values")) + { + sum += x.get(); + } + return sum; +} +} // namespace + +TEST_CASE("json_view modes") +{ + using nlohmann::basic_json_document; + using nlohmann::basic_json_view; + using nlohmann::ordered_json; + using nlohmann::view_mode; + + SECTION("read-only is the default, and the shortcuts name the modes") + { + static_assert(std::is_same, basic_json_view>::value, "default mode"); + static_assert(std::is_same, basic_json_document>::value, "default mode"); + static_assert(std::is_same>::value, "json_view"); + static_assert(std::is_same>::value, "json_editable_view"); + static_assert(std::is_same>::value, "json_editable_document"); + static_assert(std::is_same>::value, "ordered_json_editable_view"); + static_assert(std::is_same::value, "view_type"); + static_assert(std::is_same::value, "view_type"); + } + + SECTION("a function template selects views by their mode") + { + const json_document d = json_document::parse("[1]"); + const json_editable_document e = json_editable_document::parse("[1]"); + const nlohmann::ordered_json_editable_document o = nlohmann::ordered_json_editable_document::parse("[1]"); + CHECK(overload(e.root()) == 1); + CHECK(overload(o.root()) == 1); + CHECK(overload(d.root()) == 2); + CHECK(accepts_any(e.root()) == 3); + CHECK(accepts_any(d.root()) == 4); + } + + SECTION("a view of a read-only document converts to an editable view, not the other way") + { + static_assert(std::is_convertible::value, "read-only to editable"); + static_assert(!std::is_convertible::value, "not editable to read-only"); + static_assert(!std::is_convertible::value, "same BasicJsonType only"); + + const json_document d = json_document::parse(R"({"values": [1, 2, 3], "name": "a"})"); + json_editable_document e = json_editable_document::parse(R"({"values": [1, 2, 3], "name": "a"})"); + CHECK(sum_of(d.root()) == 6); + CHECK(sum_of(e.root()) == 6); + + const json_editable_view v = d.root(); + CHECK(v.size() == 2); + CHECK(v.at("name").get() == "a"); + CHECK(v.source_offset() == d.root().source_offset()); + CHECK((v == e.root())); + CHECK((v == d.root())); + + e.push_back(e.root().at("values"), 4); + CHECK(sum_of(e.root()) == 10); + CHECK((v != e.root())); + +#if !defined(JSON_NOEXCEPTION) + // the converted view still belongs to the read-only document + CHECK_THROWS_WITH_AS(e.push_back(v.at("values"), 5), "[json.exception.invalid_iterator.202] view does not belong to this document", json::invalid_iterator&); +#endif + CHECK(sum_of(d.root()) == 6); + } +} + TEST_CASE("json_view edits: views and values") { SECTION("a value that is no longer part of the document") @@ -100,7 +207,7 @@ TEST_CASE("json_view edits: views and values") // doubles have their own path to the output; other float types are // written as basic_json writes them, non-finite values as null using json_float = nlohmann::basic_json; - using document_float = nlohmann::basic_json_document; + using document_float = nlohmann::basic_json_document; document_float d = document_float::parse("[1.5]"); d.push_back(d.root(), std::numeric_limits::quiet_NaN()); d.push_back(d.root(), -std::numeric_limits::infinity()); diff --git a/tests/src/unit-json_view_image.cpp b/tests/src/unit-json_view_image.cpp index eaf27eb9f..e2a738a4a 100644 --- a/tests/src/unit-json_view_image.cpp +++ b/tests/src/unit-json_view_image.cpp @@ -480,7 +480,7 @@ TEST_CASE("json_view images: edited documents") { CAPTURE(i) CHECK((node_at(image, i).flags & nlohmann::detail::view::node_flags::linked) == 0); - CHECK(node_at(image, i).kind != nlohmann::detail::view::kind_link); + CHECK((node_at(image, i).kind != nlohmann::detail::view::node_kind::link)); } for (const image_check check : { @@ -810,13 +810,13 @@ TEST_CASE("json_view images: check") { rejected(corrupted(image, 12, [&](node & n) { - n.kind = kind; + n.kind = static_cast(kind); }), true); } // a key that is not a string rejected(corrupted(image, 1, [](node & n) { - n.kind = 0; + n.kind = nlohmann::detail::view::node_kind::null; n.len = 0; n.off = 0; }), true); @@ -960,11 +960,11 @@ TEST_CASE("json_view images: check") }), true); rejected(corrupted(image, 0, [](node & n) { - n.kind = 2; // an array: the "keys" are values, and the counts do not match + n.kind = nlohmann::detail::view::node_kind::array; // an array: the "keys" are values, and the counts do not match }), true); const std::vector as_array = corrupted(image, 16, [](node & n) { - n.kind = 2; // {} as []: fine + n.kind = nlohmann::detail::view::node_kind::array; // {} as []: fine }); CHECK(load_result(as_array, image_check::full).empty()); const std::string expected_dump = R"({"s":"x\"y","i":-12,"u":7,"f":1.5e+300,"b":true,"n":null,"a":["t",[]]})"; @@ -1013,13 +1013,13 @@ TEST_CASE("json_view images: check") rejected(at_token(u6, 0, '8'), false); // 8 != 7 rejected(corrupted(image, 4, [](node & n) { - n.kind = 6; // "-12" as unsigned: a sign + n.kind = nlohmann::detail::view::node_kind::number_unsigned; // "-12" as unsigned: a sign n.extra = 3; }), false); // a non-negative number_integer (as edits write it): fine const std::vector positive = corrupted(image, 6, [](node & n) { - n.kind = 5; + n.kind = nlohmann::detail::view::node_kind::number_integer; n.extra = 0; }); CHECK(load_result(positive, image_check::full).empty()); @@ -1027,7 +1027,7 @@ TEST_CASE("json_view images: check") CHECK(pos.root()["u"].is_number_integer()); rejected(corrupted(image, 8, [](node & n) { - n.kind = 6; // a float token as integer + n.kind = nlohmann::detail::view::node_kind::number_unsigned; // a float token as integer n.extra = 7; }), false); } @@ -1197,7 +1197,7 @@ TEST_CASE("json_view images: check") { std::vector b = img; node n = node_at(b, i); - n.kind = kind; + n.kind = static_cast(kind); n.extra = extra; set_node(b, i, n); return load_result(b, image_check::full); From 4cdfa6d0f486a76a211093918e5a26cc11f4640c Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Mon, 26 Oct 2026 20:13:30 +0100 Subject: [PATCH 14/18] json_view: no save() and load() on big-endian targets (#5920) The image format is little-endian only. Instead of throwing type_error.320 at runtime, save_image(), load_image() and the members save() and load() do not exist on a big-endian target, so a call fails to compile, like set() on a read-only document. type_error.320 remains for saving a discarded document. Signed-off-by: Niels Lohmann --- .../docs/api/basic_json_document/load.md | 8 +++---- .../docs/api/basic_json_document/save.md | 12 +++------- docs/mkdocs/docs/features/json_view.md | 7 +++--- docs/mkdocs/docs/home/exceptions.md | 5 ---- include/nlohmann/detail/exceptions.hpp | 2 +- include/nlohmann/detail/view/image.hpp | 10 ++++---- include/nlohmann/json_view.hpp | 5 ++++ single_include/nlohmann/json.hpp | 2 +- single_include/nlohmann/json_view.hpp | 15 +++++++----- tests/src/unit-json_view_image.cpp | 23 ++++++++++++++++++- 10 files changed, 53 insertions(+), 36 deletions(-) diff --git a/docs/mkdocs/docs/api/basic_json_document/load.md b/docs/mkdocs/docs/api/basic_json_document/load.md index 36223f63c..f389c0581 100644 --- a/docs/mkdocs/docs/api/basic_json_document/load.md +++ b/docs/mkdocs/docs/api/basic_json_document/load.md @@ -55,10 +55,7 @@ validate with overload 2 first, if the original vector must survive a failed loa ## Exceptions -On a big-endian target, throws [`type_error.320`](../../home/exceptions.md#jsonexceptiontype_error320) -- the same -exception [`save()`](save.md#exceptions) throws there, since the image format is little-endian only. - -Otherwise throws [`parse_error.116`](../../home/exceptions.md#jsonexceptionparse_error116) if `image` is not one +Throws [`parse_error.116`](../../home/exceptions.md#jsonexceptionparse_error116) if `image` is not one `save()` could have written, or fails the requested `check`: | message | when | @@ -95,6 +92,9 @@ are checked once, however many nodes refer to it. ## Notes +**Little-endian targets only.** On a big-endian target, `load()` does not exist, just like +[`save()`](save.md#notes): the image format is little-endian, so a call fails to compile. + ### `image_check` { #image_check } ```cpp diff --git a/docs/mkdocs/docs/api/basic_json_document/save.md b/docs/mkdocs/docs/api/basic_json_document/save.md index c47295048..c87d90f07 100644 --- a/docs/mkdocs/docs/api/basic_json_document/save.md +++ b/docs/mkdocs/docs/api/basic_json_document/save.md @@ -31,9 +31,6 @@ Throws [`type_error.320`](../../home/exceptions.md#jsonexceptiontype_error320) i [discarded](is_discarded.md) -- a default-constructed document, or one a failed [`parse()`](parse.md)/ [`read()`](read.md) with `allow_exceptions == false` left discarded. -On a big-endian target, throws `type_error.320` with a different message instead: the image format is little-endian -only (see [Notes](#notes)). - Throws [`out_of_range.416`](../../home/exceptions.md#jsonexceptionout_of_range416) if the node count, the text, or the decoded strings of the image would individually reach 4 GiB -- the same 32-bit offsets [`parse()`](parse.md#exceptions) and, for edits, [`set`](set.md)/[`push_back`](push_back.md) are already limited to. @@ -44,9 +41,6 @@ the decoded strings of the image would individually reach 4 GiB -- the same 32-b [json.exception.type_error.320] cannot save a discarded json_document ``` ``` - [json.exception.type_error.320] json_document images need a little-endian target - ``` - ``` [json.exception.out_of_range.416] images of 4 GiB or more are not supported by json_document ``` @@ -70,9 +64,9 @@ else -- see [`load`'s Exceptions](load.md#exceptions). the *same* build on the *same* (little-endian) machine -- not as a long-term storage format. Keep the original JSON text if you need to read a saved document back with a future library version. -**Little-endian only.** The image is written as raw little-endian bytes, with no byte-swapping. `save()` (and -[`load`](load.md)) throw `type_error.320` on a big-endian target rather than silently produce bytes a big-endian -reader could not interpret correctly. +**Little-endian targets only.** The image is written as raw little-endian bytes, with no byte-swapping. On a +big-endian target, `save()` and [`load`](load.md) do not exist: a call fails to compile rather than produce bytes a +big-endian reader could not interpret correctly. ## Examples diff --git a/docs/mkdocs/docs/features/json_view.md b/docs/mkdocs/docs/features/json_view.md index 4b5498af5..89e26bdb3 100644 --- a/docs/mkdocs/docs/features/json_view.md +++ b/docs/mkdocs/docs/features/json_view.md @@ -354,9 +354,10 @@ validation entirely, for an image trusted as much as the process's own memory. S !!! warning "Experimental" The image format is versioned but not yet stable, and may change in an incompatible way before it is declared - stable. It is little-endian only, and tied to the library build that wrote it -- use it to cache a document or to - hand one to another process running the *same* build, not as a long-term storage format; keep the original JSON - text if a saved document needs to be readable by a future library version. + stable. It is little-endian only (on big-endian targets, `save()` and `load()` do not exist), and tied to the + library build that wrote it -- use it to cache a document or to hand one to another process running the *same* + build, not as a long-term storage format; keep the original JSON text if a saved document needs to be readable by + a future library version. The idea of a document you can read without parsing comes from zero-copy formats such as [FlatBuffers](https://github.com/google/flatbuffers) and [YaFF](https://github.com/yandex/yaff); the check diff --git a/docs/mkdocs/docs/home/exceptions.md b/docs/mkdocs/docs/home/exceptions.md index 65b31c93c..aa2afe6ee 100644 --- a/docs/mkdocs/docs/home/exceptions.md +++ b/docs/mkdocs/docs/home/exceptions.md @@ -895,17 +895,12 @@ from JSON text. [`basic_json_document::save()`](../api/basic_json_document/save.md) cannot write an [image](../features/json_view.md#images) of a [discarded](../api/basic_json_document/is_discarded.md) document. -[`save()`](../api/basic_json_document/save.md) and [`load()`](../api/basic_json_document/load.md) also throw this -exception on a big-endian target, since the image format is little-endian only. !!! failure "Example messages" ``` [json.exception.type_error.320] cannot save a discarded json_document ``` - ``` - [json.exception.type_error.320] json_document images need a little-endian target - ``` !!! note diff --git a/include/nlohmann/detail/exceptions.hpp b/include/nlohmann/detail/exceptions.hpp index cdef3019f..d47ed6f17 100644 --- a/include/nlohmann/detail/exceptions.hpp +++ b/include/nlohmann/detail/exceptions.hpp @@ -102,7 +102,7 @@ enum class exception_id : int type_not_serializable = 317, ///< a value cannot be serialized to the requested binary format enum_key_duplicate = 318, ///< an enum-keyed map has two keys that serialize to the same string document_binary_value = 319, ///< a binary value is stored in a json_document - document_image_unsupported = 320, ///< a json_document image cannot be saved or loaded (discarded document, big-endian target) + document_image_unsupported = 320, ///< a discarded json_document cannot be saved as an image discarded_value_used = 321, ///< a discarded value is used // out_of_range array_index_out_of_range = 401, ///< an array index is out of range diff --git a/include/nlohmann/detail/view/image.hpp b/include/nlohmann/detail/view/image.hpp index c13d269e1..637350949 100644 --- a/include/nlohmann/detail/view/image.hpp +++ b/include/nlohmann/detail/view/image.hpp @@ -182,12 +182,10 @@ inline void compact_nodes(const document_data& d, std::size_t arena_size, std::v } } +#if NLOHMANN_VIEW_LITTLE_ENDIAN // (the image format is little-endian: no save() elsewhere) /// the document as an image inline std::vector save_image(const document_data& d) { -#if !NLOHMANN_VIEW_LITTLE_ENDIAN - throw_type_error(exception_id::document_image_unsupported, "json_document images need a little-endian target"); // LCOV_EXCL_LINE -#endif const std::size_t arena_size = d.arena_size; std::vector compacted; std::string text_tail; @@ -243,6 +241,7 @@ inline std::vector save_image(const document_data& d) *o = 0; return image; } +#endif /// the parts of a number token that the checks need struct number_token @@ -663,13 +662,11 @@ inline bool check_image(const node* nodes, std::size_t count, const unsigned cha throw_parse_error(exception_id::invalid_document_image, concat("invalid json_document image: ", what)); } +#if NLOHMANN_VIEW_LITTLE_ENDIAN // (the image format is little-endian: no load() elsewhere) /// Read an image into d. The text and the decoded strings stay in the image; /// the nodes are copied (so that they are aligned, and edits can change them). inline void load_image(document_data& d, const std::uint8_t* image, std::size_t size, image_check check) { -#if !NLOHMANN_VIEW_LITTLE_ENDIAN - throw_type_error(exception_id::document_image_unsupported, "json_document images need a little-endian target"); // LCOV_EXCL_LINE -#endif if (image == nullptr || size < sizeof(image_header)) { throw_invalid_image("too short"); @@ -738,6 +735,7 @@ inline void load_image(document_data& d, const std::uint8_t* image, std::size_t std::vector().swap(d.large_objects); // (only needed while building) d.discarded = false; } +#endif } // namespace view } // namespace detail diff --git a/include/nlohmann/json_view.hpp b/include/nlohmann/json_view.hpp index 42a8cb99e..79c9b8dde 100644 --- a/include/nlohmann/json_view.hpp +++ b/include/nlohmann/json_view.hpp @@ -1191,6 +1191,10 @@ class basic_json_document /// how load() checks an image (full, bounds, or none) using image_check = detail::view::image_check; +#if NLOHMANN_VIEW_LITTLE_ENDIAN + // (the image format is little-endian: big-endian targets have no save() + // and load(), so that a call fails to compile instead of always throwing) + /// The document as an image that load() reads without parsing: the node /// index, the text, and the decoded strings. An edited document is /// written in its current state (floats that are not finite become null, @@ -1232,6 +1236,7 @@ class basic_json_document detail::view::load_image(*d.m_data, d.m_data->owned_image.data(), d.m_data->owned_image.size(), check); return d; } +#endif /////////// // edits // diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index eca217237..7a68b1c54 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -5565,7 +5565,7 @@ enum class exception_id : int type_not_serializable = 317, ///< a value cannot be serialized to the requested binary format enum_key_duplicate = 318, ///< an enum-keyed map has two keys that serialize to the same string document_binary_value = 319, ///< a binary value is stored in a json_document - document_image_unsupported = 320, ///< a json_document image cannot be saved or loaded (discarded document, big-endian target) + document_image_unsupported = 320, ///< a discarded json_document cannot be saved as an image discarded_value_used = 321, ///< a discarded value is used // out_of_range array_index_out_of_range = 401, ///< an array index is out of range diff --git a/single_include/nlohmann/json_view.hpp b/single_include/nlohmann/json_view.hpp index 1fee76e63..eeb86f061 100644 --- a/single_include/nlohmann/json_view.hpp +++ b/single_include/nlohmann/json_view.hpp @@ -5032,12 +5032,10 @@ inline void compact_nodes(const document_data& d, std::size_t arena_size, std::v } } +#if NLOHMANN_VIEW_LITTLE_ENDIAN // (the image format is little-endian: no save() elsewhere) /// the document as an image inline std::vector save_image(const document_data& d) { -#if !NLOHMANN_VIEW_LITTLE_ENDIAN - throw_type_error(exception_id::document_image_unsupported, "json_document images need a little-endian target"); // LCOV_EXCL_LINE -#endif const std::size_t arena_size = d.arena_size; std::vector compacted; std::string text_tail; @@ -5093,6 +5091,7 @@ inline std::vector save_image(const document_data& d) *o = 0; return image; } +#endif /// the parts of a number token that the checks need struct number_token @@ -5513,13 +5512,11 @@ inline bool check_image(const node* nodes, std::size_t count, const unsigned cha throw_parse_error(exception_id::invalid_document_image, concat("invalid json_document image: ", what)); } +#if NLOHMANN_VIEW_LITTLE_ENDIAN // (the image format is little-endian: no load() elsewhere) /// Read an image into d. The text and the decoded strings stay in the image; /// the nodes are copied (so that they are aligned, and edits can change them). inline void load_image(document_data& d, const std::uint8_t* image, std::size_t size, image_check check) { -#if !NLOHMANN_VIEW_LITTLE_ENDIAN - throw_type_error(exception_id::document_image_unsupported, "json_document images need a little-endian target"); // LCOV_EXCL_LINE -#endif if (image == nullptr || size < sizeof(image_header)) { throw_invalid_image("too short"); @@ -5588,6 +5585,7 @@ inline void load_image(document_data& d, const std::uint8_t* image, std::size_t std::vector().swap(d.large_objects); // (only needed while building) d.discarded = false; } +#endif } // namespace view } // namespace detail @@ -8945,6 +8943,10 @@ class basic_json_document /// how load() checks an image (full, bounds, or none) using image_check = detail::view::image_check; +#if NLOHMANN_VIEW_LITTLE_ENDIAN + // (the image format is little-endian: big-endian targets have no save() + // and load(), so that a call fails to compile instead of always throwing) + /// The document as an image that load() reads without parsing: the node /// index, the text, and the decoded strings. An edited document is /// written in its current state (floats that are not finite become null, @@ -8986,6 +8988,7 @@ class basic_json_document detail::view::load_image(*d.m_data, d.m_data->owned_image.data(), d.m_data->owned_image.size(), check); return d; } +#endif /////////// // edits // diff --git a/tests/src/unit-json_view_image.cpp b/tests/src/unit-json_view_image.cpp index e2a738a4a..ff1713371 100644 --- a/tests/src/unit-json_view_image.cpp +++ b/tests/src/unit-json_view_image.cpp @@ -28,11 +28,25 @@ using nlohmann::detail::view::node; #include #include #include +#include #include #include #include +namespace json_view_image_test +{ +// whether a document type has save() and load() (only on little-endian targets) +template +struct has_save : std::false_type {}; +template +struct has_save(std::declval().save()))> : std::true_type {}; +template +struct has_load : std::false_type {}; +template +struct has_load(T::load(std::declval&>())))> : std::true_type {}; +} // namespace json_view_image_test + #if !(defined(__BYTE_ORDER__) && defined(__ORDER_BIG_ENDIAN__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__) namespace @@ -171,6 +185,9 @@ std::uint32_t rng() TEST_CASE("json_view images: round trips") { + static_assert(json_view_image_test::has_save::value && json_view_image_test::has_load::value, "save() and load() on a little-endian target"); + static_assert(json_view_image_test::has_save::value && json_view_image_test::has_load::value, "save() and load() on a little-endian target"); + SECTION("small documents") { for (const char* text : @@ -1275,8 +1292,12 @@ TEST_CASE("json_view images: damaged images") TEST_CASE("json_view images: big-endian targets") { + // the image format is little-endian: save() and load() do not exist here + static_assert(!json_view_image_test::has_save::value, "no save() on a big-endian target"); + static_assert(!json_view_image_test::has_load::value, "no load() on a big-endian target"); + static_assert(!json_view_image_test::has_save::value, "no save() on a big-endian target"); const json_document d = json_document::parse("[1]"); - CHECK_THROWS_WITH_AS(d.save(), "[json.exception.type_error.320] json_document images need a little-endian target", json::type_error&); + CHECK(d.root().size() == 1); } #endif From 5a2819c9400cfc352dc8623cccbbecf63d5ac932 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Mon, 26 Oct 2026 20:13:31 +0100 Subject: [PATCH 15/18] json_view: one SIMD macro set with the library (#5921) The view's vector code follows JSON_NO_SIMD, like the lexer and the float writer, and skips the CPU check of SSSE3 when the code is compiled for SSSE3 anyway (__SSSE3__), like the lexer. JSON_VIEW_NO_SIMD and JSON_VIEW_USE_SSSE3 and their ABI tags _vns and _vssse3 are removed; they were never released. JSON_NO_SIMD keeps its tag _ns. Signed-off-by: Niels Lohmann --- .github/workflows/check_amalgamation.yml | 6 +- FILES.md | 2 +- Makefile | 2 +- docs/mkdocs/docs/api/macros/index.md | 2 - docs/mkdocs/docs/api/macros/json_no_simd.md | 4 +- .../docs/api/macros/json_view_no_simd.md | 52 - .../docs/api/macros/json_view_use_ssse3.md | 53 - .../api/macros/nlohmann_json_namespace.md | 4 +- docs/mkdocs/docs/features/macros.md | 17 - docs/mkdocs/docs/features/namespace.md | 8 +- docs/mkdocs/docs/home/debugging.md | 4 +- docs/mkdocs/mkdocs.yml | 2 - include/nlohmann/detail/abi_macros.hpp | 22 +- include/nlohmann/detail/view/simd.hpp | 10 +- nlohmann_json.natvis | 3700 ----------------- single_include/nlohmann/json.hpp | 22 +- single_include/nlohmann/json_fwd.hpp | 22 +- single_include/nlohmann/json_view.hpp | 10 +- tests/CMakeLists.txt | 5 +- tests/abi/config/CMakeLists.txt | 4 +- tests/abi/config/default.cpp | 8 - tests/abi/config/noversion.cpp | 8 - tools/generate_natvis/README.md | 8 +- tools/generate_natvis/generate_natvis.py | 2 +- 24 files changed, 44 insertions(+), 3933 deletions(-) delete mode 100644 docs/mkdocs/docs/api/macros/json_view_no_simd.md delete mode 100644 docs/mkdocs/docs/api/macros/json_view_use_ssse3.md diff --git a/.github/workflows/check_amalgamation.yml b/.github/workflows/check_amalgamation.yml index 8104a20af..910eeef0f 100644 --- a/.github/workflows/check_amalgamation.yml +++ b/.github/workflows/check_amalgamation.yml @@ -163,9 +163,9 @@ jobs: python3 $GENERATOR --list-tags TAG_COUNT=$(python3 $GENERATOR --list-tags | wc -l) - python3 $GENERATOR --abi-tags diag,_dp,_vns --abi-tags _diag --output $RUNNER_TEMP/abi_tags.natvis - grep -q '' $RUNNER_TEMP/abi_tags.natvis - grep -q '' $RUNNER_TEMP/abi_tags.natvis + python3 $GENERATOR --abi-tags diag,_dp,_ns --abi-tags _diag --output $RUNNER_TEMP/abi_tags.natvis + grep -q '' $RUNNER_TEMP/abi_tags.natvis + grep -q '' $RUNNER_TEMP/abi_tags.natvis test $(grep -c ' - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - null @@ -3631,302 +3335,6 @@ - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - null @@ -5259,302 +4667,6 @@ - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - null @@ -6739,302 +5851,6 @@ - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - null @@ -8071,302 +6887,6 @@ - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - null @@ -9255,302 +7775,6 @@ - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - null @@ -10291,302 +8515,6 @@ - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - null @@ -11179,302 +9107,6 @@ - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - null @@ -11919,302 +9551,6 @@ - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - null @@ -12511,302 +9847,6 @@ - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - null @@ -12955,744 +9995,4 @@ - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 7a68b1c54..b6281565f 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -193,26 +193,14 @@ #define NLOHMANN_JSON_ABI_TAG_NO_SIMD #endif -#ifdef JSON_VIEW_NO_SIMD - #define NLOHMANN_JSON_ABI_TAG_VIEW_NO_SIMD _vns -#else - #define NLOHMANN_JSON_ABI_TAG_VIEW_NO_SIMD -#endif - -#ifdef JSON_VIEW_USE_SSSE3 - #define NLOHMANN_JSON_ABI_TAG_VIEW_USE_SSSE3 _vssse3 -#else - #define NLOHMANN_JSON_ABI_TAG_VIEW_USE_SSSE3 -#endif - #ifndef NLOHMANN_JSON_NAMESPACE_NO_VERSION #define NLOHMANN_JSON_NAMESPACE_NO_VERSION 0 #endif // Construct the namespace ABI tags component -#define NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b, c, d, e, f, g, h, i, j, k, l, m) json_abi ## a ## b ## c ## d ## e ## f ## g ## h ## i ## j ## k ## l ## m -#define NLOHMANN_JSON_ABI_TAGS_CONCAT(a, b, c, d, e, f, g, h, i, j, k, l, m) \ - NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b, c, d, e, f, g, h, i, j, k, l, m) +#define NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b, c, d, e, f, g, h, i, j, k) json_abi ## a ## b ## c ## d ## e ## f ## g ## h ## i ## j ## k +#define NLOHMANN_JSON_ABI_TAGS_CONCAT(a, b, c, d, e, f, g, h, i, j, k) \ + NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b, c, d, e, f, g, h, i, j, k) #define NLOHMANN_JSON_ABI_TAGS \ NLOHMANN_JSON_ABI_TAGS_CONCAT( \ @@ -226,9 +214,7 @@ NLOHMANN_JSON_ABI_TAG_OBJECTS_FOR_ENUM_KEYED_MAPS, \ NLOHMANN_JSON_ABI_TAG_DISABLE_STREAM_BULK_SCAN, \ NLOHMANN_JSON_ABI_TAG_DISABLE_UNLOCKED_FILE_IO, \ - NLOHMANN_JSON_ABI_TAG_NO_SIMD, \ - NLOHMANN_JSON_ABI_TAG_VIEW_NO_SIMD, \ - NLOHMANN_JSON_ABI_TAG_VIEW_USE_SSSE3) + NLOHMANN_JSON_ABI_TAG_NO_SIMD) // Construct the namespace version component #define NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT_EX(major, minor, patch) \ diff --git a/single_include/nlohmann/json_fwd.hpp b/single_include/nlohmann/json_fwd.hpp index cd64b76a6..ce51d81bd 100644 --- a/single_include/nlohmann/json_fwd.hpp +++ b/single_include/nlohmann/json_fwd.hpp @@ -145,26 +145,14 @@ #define NLOHMANN_JSON_ABI_TAG_NO_SIMD #endif -#ifdef JSON_VIEW_NO_SIMD - #define NLOHMANN_JSON_ABI_TAG_VIEW_NO_SIMD _vns -#else - #define NLOHMANN_JSON_ABI_TAG_VIEW_NO_SIMD -#endif - -#ifdef JSON_VIEW_USE_SSSE3 - #define NLOHMANN_JSON_ABI_TAG_VIEW_USE_SSSE3 _vssse3 -#else - #define NLOHMANN_JSON_ABI_TAG_VIEW_USE_SSSE3 -#endif - #ifndef NLOHMANN_JSON_NAMESPACE_NO_VERSION #define NLOHMANN_JSON_NAMESPACE_NO_VERSION 0 #endif // Construct the namespace ABI tags component -#define NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b, c, d, e, f, g, h, i, j, k, l, m) json_abi ## a ## b ## c ## d ## e ## f ## g ## h ## i ## j ## k ## l ## m -#define NLOHMANN_JSON_ABI_TAGS_CONCAT(a, b, c, d, e, f, g, h, i, j, k, l, m) \ - NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b, c, d, e, f, g, h, i, j, k, l, m) +#define NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b, c, d, e, f, g, h, i, j, k) json_abi ## a ## b ## c ## d ## e ## f ## g ## h ## i ## j ## k +#define NLOHMANN_JSON_ABI_TAGS_CONCAT(a, b, c, d, e, f, g, h, i, j, k) \ + NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b, c, d, e, f, g, h, i, j, k) #define NLOHMANN_JSON_ABI_TAGS \ NLOHMANN_JSON_ABI_TAGS_CONCAT( \ @@ -178,9 +166,7 @@ NLOHMANN_JSON_ABI_TAG_OBJECTS_FOR_ENUM_KEYED_MAPS, \ NLOHMANN_JSON_ABI_TAG_DISABLE_STREAM_BULK_SCAN, \ NLOHMANN_JSON_ABI_TAG_DISABLE_UNLOCKED_FILE_IO, \ - NLOHMANN_JSON_ABI_TAG_NO_SIMD, \ - NLOHMANN_JSON_ABI_TAG_VIEW_NO_SIMD, \ - NLOHMANN_JSON_ABI_TAG_VIEW_USE_SSSE3) + NLOHMANN_JSON_ABI_TAG_NO_SIMD) // Construct the namespace version component #define NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT_EX(major, minor, patch) \ diff --git a/single_include/nlohmann/json_view.hpp b/single_include/nlohmann/json_view.hpp index eeb86f061..0e6c087ab 100644 --- a/single_include/nlohmann/json_view.hpp +++ b/single_include/nlohmann/json_view.hpp @@ -678,25 +678,25 @@ NLOHMANN_JSON_NAMESPACE_END // with different flags would have different definitions of the same inline // functions): the check is compiled for SSSE3 with a function attribute and // used where the CPU has SSSE3 (all x86-64 CPUs since about 2011), else the -// portable check. JSON_VIEW_USE_SSSE3 skips the CPU check (for code compiled -// for SSSE3 anyway); JSON_VIEW_NO_SIMD selects the portable code. +// portable check. Code compiled for SSSE3 anyway (-mssse3 or more) skips the +// CPU check. JSON_NO_SIMD selects the portable code, as in the lexer. // NLOHMANN_VIEW_NO_TARGET_ATTRIBUTE (set by the GCC module interface, where GCC // ignores the target attribute and then rejects the SSSE3 intrinsics) keeps the // check portable without disabling the SSE2 code. -#if !defined(JSON_VIEW_NO_SIMD) && defined(__aarch64__) && (defined(__GNUC__) || defined(__clang__)) && NLOHMANN_VIEW_LITTLE_ENDIAN +#if !defined(JSON_NO_SIMD) && defined(__aarch64__) && (defined(__GNUC__) || defined(__clang__)) && NLOHMANN_VIEW_LITTLE_ENDIAN #include #define NLOHMANN_VIEW_NEON 1 #else #define NLOHMANN_VIEW_NEON 0 #endif // (x86 only: other targets can define __SSE2__ as well, e.g., WebAssembly with -msse2, but have no ) -#if !defined(JSON_VIEW_NO_SIMD) && !NLOHMANN_VIEW_NEON && (defined(__x86_64__) || defined(__i386__) || defined(_M_X64) || defined(_M_IX86)) && (defined(__SSE2__) || defined(_M_X64) || (defined(_M_IX86_FP) && _M_IX86_FP >= 2)) +#if !defined(JSON_NO_SIMD) && !NLOHMANN_VIEW_NEON && (defined(__x86_64__) || defined(__i386__) || defined(_M_X64) || defined(_M_IX86)) && (defined(__SSE2__) || defined(_M_X64) || (defined(_M_IX86_FP) && _M_IX86_FP >= 2)) #include #define NLOHMANN_VIEW_SSE2 1 #else #define NLOHMANN_VIEW_SSE2 0 #endif -#if NLOHMANN_VIEW_SSE2 && defined(JSON_VIEW_USE_SSSE3) +#if NLOHMANN_VIEW_SSE2 && defined(__SSSE3__) #include #define NLOHMANN_VIEW_SSSE3 1 // NOLINT(cppcoreguidelines-macro-to-enum,modernize-macro-to-enum) #else diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index ee7e07f08..f4e574fc1 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -420,9 +420,9 @@ json_test_add_test_for(src/unit-file_input.cpp ) # the json_view parser again with the portable string scanning instead of -# NEON/SSE2, and on x86-64 with the SSSE3 UTF-8 check (JSON_VIEW_USE_SSSE3) +# NEON/SSE2 (JSON_NO_SIMD), and on x86-64 compiled for SSSE3 (no CPU check) json_test_set_test_options(test-json_view_builder_portable - COMPILE_DEFINITIONS JSON_VIEW_NO_SIMD + COMPILE_DEFINITIONS JSON_NO_SIMD ) json_test_add_test_for(src/unit-json_view_builder.cpp NAME test-json_view_builder_portable @@ -430,7 +430,6 @@ json_test_add_test_for(src/unit-json_view_builder.cpp ) if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|AMD64|amd64)$" AND NOT MSVC) json_test_set_test_options(test-json_view_builder_ssse3 - COMPILE_DEFINITIONS JSON_VIEW_USE_SSSE3 COMPILE_OPTIONS -mssse3 ) json_test_add_test_for(src/unit-json_view_builder.cpp diff --git a/tests/abi/config/CMakeLists.txt b/tests/abi/config/CMakeLists.txt index 930afeecc..f3f27c2ee 100644 --- a/tests/abi/config/CMakeLists.txt +++ b/tests/abi/config/CMakeLists.txt @@ -24,9 +24,7 @@ foreach(test default noversion) JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON=1 JSON_DISABLE_STREAM_BULK_SCAN=1 JSON_DISABLE_UNLOCKED_FILE_IO=1 - JSON_NO_SIMD - JSON_VIEW_NO_SIMD - JSON_VIEW_USE_SSSE3) + JSON_NO_SIMD) target_link_libraries(abi_config_${test}_all_tags PRIVATE abi_compat_main) add_test( NAME test-abi_config_${test}_all_tags diff --git a/tests/abi/config/default.cpp b/tests/abi/config/default.cpp index a9fa472d3..084edf8c9 100644 --- a/tests/abi/config/default.cpp +++ b/tests/abi/config/default.cpp @@ -64,14 +64,6 @@ TEST_CASE("default namespace") expected += "_ns"; #endif -#ifdef JSON_VIEW_NO_SIMD - expected += "_vns"; -#endif - -#ifdef JSON_VIEW_USE_SSSE3 - expected += "_vssse3"; -#endif - expected += "_v" STRINGIZE(NLOHMANN_JSON_VERSION_MAJOR); expected += "_" STRINGIZE(NLOHMANN_JSON_VERSION_MINOR); expected += "_" STRINGIZE(NLOHMANN_JSON_VERSION_PATCH) "::basic_json"; diff --git a/tests/abi/config/noversion.cpp b/tests/abi/config/noversion.cpp index a090d1d02..66164ff88 100644 --- a/tests/abi/config/noversion.cpp +++ b/tests/abi/config/noversion.cpp @@ -65,14 +65,6 @@ TEST_CASE("default namespace without version component") expected += "_ns"; #endif -#ifdef JSON_VIEW_NO_SIMD - expected += "_vns"; -#endif - -#ifdef JSON_VIEW_USE_SSSE3 - expected += "_vssse3"; -#endif - expected += "::basic_json"; // fallback for Clang diff --git a/tools/generate_natvis/README.md b/tools/generate_natvis/README.md index 8f45f1877..cc4fc4a8f 100644 --- a/tools/generate_natvis/README.md +++ b/tools/generate_natvis/README.md @@ -34,11 +34,11 @@ version other than the one in `abi_macros.hpp`. | Option | Description | |-------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `--list-tags` | Print every ABI tag (in namespace order) with the macro that enables it and the `NLOHMANN_JSON_ABI_TAG_*` identifier, then exit. | -| `--abi-tags TAGS` | Add the namespaces of one configuration, given as a comma-separated list of ABI tags (`_diag,_dp,_vns`; the leading underscore is optional, and the order does not matter). Can be repeated for several configurations. Unknown tags are an error. | +| `--abi-tags TAGS` | Add the namespaces of one configuration, given as a comma-separated list of ABI tags (`_diag,_dp,_ns`; the leading underscore is optional, and the order does not matter). Can be repeated for several configurations. Unknown tags are an error. | | `--all` | Generate the namespaces for every combination of ABI tags. The file size doubles with each tag (about 60 MB for 12 tags). Cannot be combined with `--abi-tags`. | | `--output FILE` | Write the Natvis file to `FILE` instead of `nlohmann_json.natvis` in the repository root. | -Each configuration gets both the versioned (`json_abi_diag_dp_vns_v3_12_0`) and the unversioned (`json_abi_diag_dp_vns`, +Each configuration gets both the versioned (`json_abi_diag_dp_ns_v3_12_0`) and the unversioned (`json_abi_diag_dp_ns`, used with `NLOHMANN_JSON_NAMESPACE_NO_VERSION`) namespace. The default namespaces (up to two ABI tags) are always included, so a generated file can replace the checked-in one. @@ -52,10 +52,10 @@ included, so a generated file can replace the checked-in one. ``` 3. Generate a Natvis file with them, for example for `JSON_DIAGNOSTICS`, `JSON_DIAGNOSTIC_POSITIONS`, and - `JSON_VIEW_NO_SIMD`: + `JSON_NO_SIMD`: ```shell - make natvis NATVIS_ARGS="--abi-tags _diag,_dp,_vns --output my_nlohmann_json.natvis" + make natvis NATVIS_ARGS="--abi-tags _diag,_dp,_ns --output my_nlohmann_json.natvis" ``` 4. Use `my_nlohmann_json.natvis` instead of the shipped `nlohmann_json.natvis`, for example by adding it to your Visual diff --git a/tools/generate_natvis/generate_natvis.py b/tools/generate_natvis/generate_natvis.py index d660c4b63..8452d98e0 100755 --- a/tools/generate_natvis/generate_natvis.py +++ b/tools/generate_natvis/generate_natvis.py @@ -91,7 +91,7 @@ if __name__ == '__main__': 'include/nlohmann/detail/abi_macros.hpp below "repo_root")') selection = parser.add_mutually_exclusive_group() selection.add_argument('--abi-tags', action='append', default=[], metavar='TAGS', - help='Comma-separated list of ABI tags (e.g. "_diag,_dp,_vns", leading underscores are ' + help='Comma-separated list of ABI tags (e.g. "_diag,_dp,_ns", leading underscores are ' 'optional) of one configuration to add to the default namespaces; can be repeated') selection.add_argument('--all', action='store_true', help='Generate the namespaces for every combination of ABI tags; the file size doubles ' From e5fe4ebdd2304d9087bb1f351570563ed80e5f31 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Mon, 26 Oct 2026 20:13:31 +0100 Subject: [PATCH 16/18] json_view: from_json() of the macros reads views directly (#5922) get() and get_to() of a view call T's from_json() with the view itself when that from_json() accepts views but not an unrelated probe type, and fall back to materialize() otherwise. The from_json() functions of the NLOHMANN_DEFINE_TYPE_*, NLOHMANN_DEFINE_DERIVED_TYPE_* and NLOHMANN_JSON_SERIALIZE_ENUM(_STRICT) macros accept basic_json values and views; a derived type whose base reads only json reads the base part through materialize(). The library's own from_json() overloads are not called with views: from_json() of a view (and of the probe) only looks up functions by ADL. New public trait nlohmann::is_json_view, and the member type basic_json_type of basic_json_view. With duplicate keys, the members of such a T are those the view's lookups find (the first ones). bench_decode.cpp measures decoding into structs (4-6x faster than materialize().get() on twitter, citm_catalog, and synthetic records). Signed-off-by: Niels Lohmann --- docs/mkdocs/docs/api/basic_json_view/get.md | 28 +- .../mkdocs/docs/api/basic_json_view/get_to.md | 4 +- docs/mkdocs/docs/api/basic_json_view/index.md | 1 + docs/mkdocs/docs/api/is_json_view.md | 55 +++ .../macros/nlohmann_define_derived_type.md | 9 + .../macros/nlohmann_define_type_intrusive.md | 8 + .../nlohmann_define_type_non_intrusive.md | 8 + .../macros/nlohmann_json_serialize_enum.md | 8 + .../nlohmann_json_serialize_enum_strict.md | 6 + docs/mkdocs/docs/examples/is_json_view.cpp | 34 ++ docs/mkdocs/docs/examples/is_json_view.output | 2 + docs/mkdocs/docs/features/json_view.md | 4 + docs/mkdocs/mkdocs.yml | 1 + .../nlohmann/detail/conversions/from_json.hpp | 106 +++++- include/nlohmann/detail/macro_scope.hpp | 60 +-- include/nlohmann/detail/meta/type_traits.hpp | 41 ++ include/nlohmann/json_view.hpp | 52 ++- single_include/nlohmann/json.hpp | 310 +++++++++++---- single_include/nlohmann/json_view.hpp | 52 ++- tests/benchmarks/json_view/README.md | 6 + tests/benchmarks/json_view/bench_decode.cpp | 312 +++++++++++++++ tests/benchmarks/json_view/compare.py | 8 +- tests/src/unit-json_view_from_json.cpp | 357 ++++++++++++++++++ 23 files changed, 1342 insertions(+), 130 deletions(-) create mode 100644 docs/mkdocs/docs/api/is_json_view.md create mode 100644 docs/mkdocs/docs/examples/is_json_view.cpp create mode 100644 docs/mkdocs/docs/examples/is_json_view.output create mode 100644 tests/benchmarks/json_view/bench_decode.cpp create mode 100644 tests/src/unit-json_view_from_json.cpp diff --git a/docs/mkdocs/docs/api/basic_json_view/get.md b/docs/mkdocs/docs/api/basic_json_view/get.md index 7fe30ca7f..40fd1033a 100644 --- a/docs/mkdocs/docs/api/basic_json_view/get.md +++ b/docs/mkdocs/docs/api/basic_json_view/get.md @@ -27,8 +27,25 @@ For the types below, the conversion works directly on the flat index -- no `Basi [`materialize()`](materialize.md)) does; `#!cpp std::map` keeps views of the members instead of values -Every other `T` -- `#!cpp std::list`, `#!cpp std::pair`, `#!cpp std::array`, enumerations, user types with a -`from_json()`, ... -- is converted by `#!cpp materialize().get()`: the subtree is built into a real `BasicJsonType` +A default-constructible `T` whose `from_json()` accepts views reads the view directly as well, member by member, without +building a `BasicJsonType` value. This covers the types of the +[`NLOHMANN_DEFINE_TYPE_*`](../macros/nlohmann_define_type_intrusive.md) and +[`NLOHMANN_DEFINE_DERIVED_TYPE_*`](../macros/nlohmann_define_derived_type.md) macros and the enumerations of +[`NLOHMANN_JSON_SERIALIZE_ENUM`](../macros/nlohmann_json_serialize_enum.md), and any `from_json()` constrained to +accept views, e.g. with [`is_json_view`](../is_json_view.md). A `from_json()` that also accepts types that are no JSON +values at all, such as the unconstrained `#!cpp template void from_json(const BasicJsonType&, +T&)` of [arbitrary type conversions](../../features/arbitrary_types.md), is not called with a view: it may use +`BasicJsonType` functions a view does not have. See [json_view](../../features/json_view.md). + +!!! warning "Duplicate keys" + + Such a `from_json()` looks members up in the view, where a repeated key finds the **first** member, while + `#!cpp materialize().get()` sees the **last** one, as [`BasicJsonType::parse()`](../basic_json/parse.md) + keeps it: for `#!cpp {"x":1,"x":2}`, `#!cpp get()` reads `#!cpp x == 1`. See + [Duplicate keys](../../features/json_view.md#duplicate-keys). + +Every other `T` -- `#!cpp std::list`, `#!cpp std::pair`, `#!cpp std::array`, user types whose `from_json()` is +written for `BasicJsonType` only, ... -- is converted by `#!cpp materialize().get()`: the subtree is built into a real `BasicJsonType` value first (as [`BasicJsonType::parse()`](../basic_json/parse.md) would), and converted from there exactly as [`BasicJsonType::get()`](../basic_json/get.md) would convert it. @@ -55,10 +72,13 @@ Strong guarantee: there are no changes to the view or the document it refers to. an element to `U` throws. - For `#!cpp std::map`/`#!cpp std::unordered_map`: throws `type_error.302` if the value is not an object; otherwise, whatever converting a member to the mapped type throws. +- For a `T` whose `from_json()` reads the view: whatever that `from_json()` throws; for the macros, the same + exceptions with the same messages as [`BasicJsonType::get()`](../basic_json/get.md) of the same value. - For every other `T`: whatever [`materialize().get()`](../basic_json/get.md) throws -- typically `type_error.302`, or whatever a user-provided `from_json()` throws. -None of the exceptions thrown directly by this function (the first three bullets above) carry a +None of the exceptions thrown directly by this function or by a `from_json()` that reads the view (the first four +bullets above) carry a [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) path: the view has no `BasicJsonType` value to point at. An exception thrown while converting through `materialize()` (the last bullet) is different: it is thrown by a real `BasicJsonType` value, so it **does** carry a `JSON_DIAGNOSTICS` path if `BasicJsonType` was built with it enabled. @@ -76,6 +96,8 @@ document. - `#!cpp std::map`/`#!cpp std::unordered_map`: linear in the number of members for walking them, plus the container's own insertion cost per member (logarithmic for `#!cpp std::map`, amortized constant for `#!cpp std::unordered_map`), times the complexity of converting one member to the mapped type. +- a `T` whose `from_json()` reads the view: the cost of that `from_json()`; for the macros, linear in the number of + members times the cost of a lookup, plus the cost of converting the members. - every other `T`: the cost of [`materialize()`](materialize.md) and of destroying the `BasicJsonType` value afterwards, plus that of [`BasicJsonType::get()`](../basic_json/get.md). diff --git a/docs/mkdocs/docs/api/basic_json_view/get_to.md b/docs/mkdocs/docs/api/basic_json_view/get_to.md index 4f1c6a5d9..4d2dd2fa5 100644 --- a/docs/mkdocs/docs/api/basic_json_view/get_to.md +++ b/docs/mkdocs/docs/api/basic_json_view/get_to.md @@ -5,12 +5,14 @@ template T& get_to(T& v) const; ``` -Converts the value to `T` and stores it in `v`, in one of two ways: +Converts the value to `T` and stores it in `v`, in one of three ways: - For the types that [`get()`](get.md) converts directly -- arithmetic types, `#!cpp bool`, `#!cpp std::nullptr_t`, [`string_view_t`](index.md#member-types), `#!cpp std::basic_string`, `BasicJsonType`, `basic_json_view`, `#!cpp std::vector`, and `#!cpp std::map`/`#!cpp std::unordered_map` with string keys -- `v` is assigned: `#!cpp v = get()`. +- For a type whose `from_json()` accepts views (see [`get()`](get.md)), that `from_json()` is called with the + view and the existing `v`, without building a `BasicJsonType` value. - For every other type, the subtree is built into a `BasicJsonType` value and `#!cpp materialize().get_to(v)` is called, i.e. `from_json()` is invoked with the existing `v`, exactly as [`BasicJsonType::get_to()`](../basic_json/get_to.md) does. diff --git a/docs/mkdocs/docs/api/basic_json_view/index.md b/docs/mkdocs/docs/api/basic_json_view/index.md index 5049e2242..4a634b8f0 100644 --- a/docs/mkdocs/docs/api/basic_json_view/index.md +++ b/docs/mkdocs/docs/api/basic_json_view/index.md @@ -64,6 +64,7 @@ still refers to it -- including ones taken before the change -- reads the new va ## Member types +- **basic_json_type** - `BasicJsonType` (it also marks the class for [`is_json_view`](../is_json_view.md)) - **value_t** - the JSON type enumeration, see [`basic_json::value_t`](../basic_json/value_t.md) - **string_t**, **number_integer_t**, **number_unsigned_t**, **number_float_t**, **json_pointer** - the corresponding member types of `BasicJsonType` diff --git a/docs/mkdocs/docs/api/is_json_view.md b/docs/mkdocs/docs/api/is_json_view.md new file mode 100644 index 000000000..53bd8ec3a --- /dev/null +++ b/docs/mkdocs/docs/api/is_json_view.md @@ -0,0 +1,55 @@ +# nlohmann::is_json_view + +Defined in header `` + +```cpp +template +struct is_json_view; +``` + +A type trait: `#!cpp is_json_view::value` is `#!cpp true` if `T` is a [`basic_json_view`](basic_json_view/index.md) +of any `BasicJsonType` and any [`view_mode`](view_mode.md) -- [`json_view`](json_view.md), +[`json_editable_view`](json_editable_view.md), [`ordered_json_view`](ordered_json_view.md), ... -- and +`#!cpp false` otherwise, for [`basic_json`](basic_json/index.md) as well. + +It is meant for constraining templates that accept views, in particular a `from_json()` that should read views +directly: [`get()`](basic_json_view/get.md) on a view calls a `from_json()` with the view only if that `from_json()` +accepts views but not arbitrary types. + +## Template parameters + +`T` +: the type to check (without cv-qualifiers or references) + +## Notes + +A view is recognized by its member type `basic_json_type` (a [`basic_json`](basic_json/index.md) specialization) and +its [`materialize()`](basic_json_view/materialize.md) returning that type, so the trait needs no declaration of the +view class. + +## Examples + +??? example + + The example below shows the trait, and a `from_json()` constrained to `json` values and views: `get()` on a view + reads the view directly. + + ```cpp + --8<-- "examples/is_json_view.cpp" + ``` + + Output: + + ``` + --8<-- "examples/is_json_view.output" + ``` + +## See also + +- [basic_json_view](basic_json_view/index.md) - a handle to one value of a document +- [get](basic_json_view/get.md) - which conversions read the view directly +- [view_mode](view_mode.md) - read-only or editable + +## Version history + +Added in version 3.13.0. diff --git a/docs/mkdocs/docs/api/macros/nlohmann_define_derived_type.md b/docs/mkdocs/docs/api/macros/nlohmann_define_derived_type.md index c539e77a0..ef0a8c441 100644 --- a/docs/mkdocs/docs/api/macros/nlohmann_define_derived_type.md +++ b/docs/mkdocs/docs/api/macros/nlohmann_define_derived_type.md @@ -147,6 +147,13 @@ void to_json(BasicJsonType& j, const B& b) { See Implementation limits for [NLOHMANN_DEFINE_TYPE_INTRUSIVE](nlohmann_define_type_intrusive.md) and [NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE](nlohmann_define_type_non_intrusive.md), respectively. +!!! info "Views" + + The generated `from_json()` also reads the views of [`json_document`](../json_document.md)s + (``): [`get()`](../basic_json_view/get.md) on a view looks the members up in the + view, without building a `json` value first. The base part is read from the view as well if the base's + `from_json()` accepts views, and from [`materialize()`](../basic_json_view/materialize.md) otherwise. + ## Examples ??? example "Example: (1) NLOHMANN_DEFINE_DERIVED_TYPE_INTRUSIVE" @@ -192,3 +199,5 @@ void to_json(BasicJsonType& j, const B& b) { 6. Added in version 3.12.0. All six macros were changed to allow an empty member list in version 3.13.0. + +The generated `from_json()` reads the views of `json_document`s since version 3.13.0. diff --git a/docs/mkdocs/docs/api/macros/nlohmann_define_type_intrusive.md b/docs/mkdocs/docs/api/macros/nlohmann_define_type_intrusive.md index dab5c7070..b50022e05 100644 --- a/docs/mkdocs/docs/api/macros/nlohmann_define_type_intrusive.md +++ b/docs/mkdocs/docs/api/macros/nlohmann_define_type_intrusive.md @@ -81,6 +81,12 @@ See the examples below for the concrete generated code. that serializes a struct's members positionally into a JSON array; for that, write `to_json`/`from_json` by hand, building/reading a `json::array()` of the members in order. +!!! info "Views" + + The generated `from_json()` also reads the views of [`json_document`](../json_document.md)s + (``): [`get()`](../basic_json_view/get.md) on a view looks the members up in the + view, without building a `json` value first. + ## Examples ??? example "Example: (1) NLOHMANN_DEFINE_TYPE_INTRUSIVE" @@ -189,3 +195,5 @@ See the examples below for the concrete generated code. All three macros were changed to work with any `basic_json` specialization (not only `nlohmann::json`) in version 3.12.0, and to allow an empty member list in version 3.13.0. + +The generated `from_json()` reads the views of `json_document`s since version 3.13.0. diff --git a/docs/mkdocs/docs/api/macros/nlohmann_define_type_non_intrusive.md b/docs/mkdocs/docs/api/macros/nlohmann_define_type_non_intrusive.md index 07d0166d0..373af300f 100644 --- a/docs/mkdocs/docs/api/macros/nlohmann_define_type_non_intrusive.md +++ b/docs/mkdocs/docs/api/macros/nlohmann_define_type_non_intrusive.md @@ -80,6 +80,12 @@ See the examples below for the concrete generated code. that serializes a struct's members positionally into a JSON array; for that, write `to_json`/`from_json` by hand, building/reading a `json::array()` of the members in order. +!!! info "Views" + + The generated `from_json()` also reads the views of [`json_document`](../json_document.md)s + (``): [`get()`](../basic_json_view/get.md) on a view looks the members up in the + view, without building a `json` value first. + ## Examples ??? example "Example: (1) NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE" @@ -188,3 +194,5 @@ See the examples below for the concrete generated code. All three macros were changed to work with any `basic_json` specialization (not only `nlohmann::json`) in version 3.12.0, and to allow an empty member list in version 3.13.0. + +The generated `from_json()` reads the views of `json_document`s since version 3.13.0. diff --git a/docs/mkdocs/docs/api/macros/nlohmann_json_serialize_enum.md b/docs/mkdocs/docs/api/macros/nlohmann_json_serialize_enum.md index 4af3b07b8..6382996ba 100644 --- a/docs/mkdocs/docs/api/macros/nlohmann_json_serialize_enum.md +++ b/docs/mkdocs/docs/api/macros/nlohmann_json_serialize_enum.md @@ -45,6 +45,12 @@ inline void from_json(const BasicJsonType& j, type& e); Define [`JSON_USE_OBJECTS_FOR_ENUM_KEYED_MAPS`](json_use_objects_for_enum_keyed_maps.md) to store them as objects with the converted keys. Such maps can be read from both forms. +!!! info "Views" + + The generated `from_json()` also reads the views of [`json_document`](../json_document.md)s + (``), comparing the view with the entries of the mapping, without building a `json` value + first. + ## Examples ??? example "Example: Basic usage" @@ -88,3 +94,5 @@ inline void from_json(const BasicJsonType& j, type& e); ## Version history Added in version 3.4.0. + +The generated `from_json()` reads the views of `json_document`s since version 3.13.0. diff --git a/docs/mkdocs/docs/api/macros/nlohmann_json_serialize_enum_strict.md b/docs/mkdocs/docs/api/macros/nlohmann_json_serialize_enum_strict.md index f5806a490..d1513f4e7 100644 --- a/docs/mkdocs/docs/api/macros/nlohmann_json_serialize_enum_strict.md +++ b/docs/mkdocs/docs/api/macros/nlohmann_json_serialize_enum_strict.md @@ -48,6 +48,12 @@ inline void from_json(const BasicJsonType& j, type& e); Define [`JSON_USE_OBJECTS_FOR_ENUM_KEYED_MAPS`](json_use_objects_for_enum_keyed_maps.md) to store them as objects with the converted keys. Such maps can be read from both forms. +!!! info "Views" + + The generated `from_json()` also reads the views of [`json_document`](../json_document.md)s + (``), comparing the view with the entries of the mapping, without building a `json` value + first. + ## Examples ??? example "Example: Basic usage" diff --git a/docs/mkdocs/docs/examples/is_json_view.cpp b/docs/mkdocs/docs/examples/is_json_view.cpp new file mode 100644 index 000000000..c7c34ee1a --- /dev/null +++ b/docs/mkdocs/docs/examples/is_json_view.cpp @@ -0,0 +1,34 @@ +#include +#include +#include + +using nlohmann::json; + +namespace ns +{ +struct person +{ + std::string name; + int age = 0; +}; + +// reads json values and views (of any document type), but nothing else +template < typename J, typename std::enable_if < nlohmann::is_json_view::value || std::is_same::value, int >::type = 0 > +void from_json(const J& j, person& p) +{ + p.name = j.at("name").template get(); + p.age = j.at("age").template get(); +} +} // namespace ns + +int main() +{ + std::cout << std::boolalpha + << nlohmann::is_json_view::value << ' ' + << nlohmann::is_json_view::value << ' ' + << nlohmann::is_json_view::value << '\n'; + + const nlohmann::json_document doc = nlohmann::json_document::parse(R"({"name": "Ada", "age": 36})"); + const auto p = doc.root().get(); // reads the view, no json value is built + std::cout << p.name << ' ' << p.age << '\n'; +} diff --git a/docs/mkdocs/docs/examples/is_json_view.output b/docs/mkdocs/docs/examples/is_json_view.output new file mode 100644 index 000000000..df2203a21 --- /dev/null +++ b/docs/mkdocs/docs/examples/is_json_view.output @@ -0,0 +1,2 @@ +true true false +Ada 36 diff --git a/docs/mkdocs/docs/features/json_view.md b/docs/mkdocs/docs/features/json_view.md index 89e26bdb3..7de4a07cd 100644 --- a/docs/mkdocs/docs/features/json_view.md +++ b/docs/mkdocs/docs/features/json_view.md @@ -185,6 +185,10 @@ key as well. Other zero-copy parsers that index the source text, such as yyjson member. RFC 8259 only says that names within an object SHOULD be unique and that the behavior of a receiver that sees duplicates is unpredictable, so neither choice is wrong. +The same holds for [`get()`](../api/basic_json_view/get.md) of a type whose `from_json()` reads the view, such as +the types of the `NLOHMANN_DEFINE_TYPE_*` macros: their members are looked up, so `#!cpp get()` of +`#!cpp {"x":1,"x":2}` reads `#!cpp x == 1`, while `#!cpp materialize().get()` reads `#!cpp 2`. + **What stays the same as `parse()`?** [`materialize()`](../api/basic_json_view/materialize.md) and [`get>()`](../api/basic_json_view/get.md) replay every member in order, so they keep the *last* value exactly like `#!cpp basic_json::parse()` (at the position of the first occurrence of the key, for an diff --git a/docs/mkdocs/mkdocs.yml b/docs/mkdocs/mkdocs.yml index c65ca7de8..d88864f09 100644 --- a/docs/mkdocs/mkdocs.yml +++ b/docs/mkdocs/mkdocs.yml @@ -362,6 +362,7 @@ nav: - 'Overview': api/adl_serializer/index.md - 'from_json': api/adl_serializer/from_json.md - 'to_json': api/adl_serializer/to_json.md + - 'is_json_view': api/is_json_view.md - 'json': api/json.md - 'json_document': api/json_document.md - 'json_editable_document': api/json_editable_document.md diff --git a/include/nlohmann/detail/conversions/from_json.hpp b/include/nlohmann/detail/conversions/from_json.hpp index 51f26706c..696afc34b 100644 --- a/include/nlohmann/detail/conversions/from_json.hpp +++ b/include/nlohmann/detail/conversions/from_json.hpp @@ -10,6 +10,7 @@ #include // min, transform #include // array +#include // nullptr_t #include // forward_list #include // inserter, front_inserter, end #include // map @@ -27,6 +28,7 @@ #include #include #include +#include #include #include @@ -665,16 +667,114 @@ inline void from_json(const BasicJsonType& j, std_fs::path& p) } #endif -struct from_json_fn +namespace from_json_adl { - template - auto operator()(const BasicJsonType& j, T&& val) const +// hides the library's from_json() overloads above (and the from_json object): +// a call from this namespace finds only the from_json() functions of the +// arguments' own namespaces and classes (ADL); the declaration is never viable +// and never defined (not "= delete", which GCC before 6 reports as a use) +struct hidden; +void from_json(const hidden&); + +// a type no from_json() written for JSON values accepts: a from_json() that +// accepts it is not constrained to them (see has_view_from_json; it lives +// here, so that ADL finds no other from_json() for it) +struct probe +{}; + +struct fn +{ + template + auto operator()(const SourceType& j, T&& val) const noexcept(noexcept(from_json(j, std::forward(val)))) -> decltype(from_json(j, std::forward(val))) { return from_json(j, std::forward(val)); } }; +} // namespace from_json_adl + +// the sources whose from_json() is only looked up by ADL: views (the library's +// overloads are written for basic_json) and the probe +template +struct is_adl_only_from_json_source : disjunction, std::is_same> {}; + +struct from_json_fn +{ + template < typename BasicJsonType, typename T, + enable_if_t < !is_adl_only_from_json_source::value, int > = 0 > + auto operator()(const BasicJsonType& j, T && val) const + noexcept(noexcept(from_json(j, std::forward(val)))) + -> decltype(from_json(j, std::forward(val))) + { + return from_json(j, std::forward(val)); + } + + template < typename SourceType, typename T, + enable_if_t < is_adl_only_from_json_source::value, int > = 0 > + auto operator()(const SourceType& j, T && val) const + noexcept(noexcept(from_json_adl::fn {}(j, std::forward(val)))) + -> decltype(from_json_adl::fn {}(j, std::forward(val))) + { + return from_json_adl::fn {}(j, std::forward(val)); + } +}; + +// Whether JSONSerializer::from_json(view, T&) reads T from a view (of +// ) directly: it must accept the view but not the +// probe. An unconstrained from_json() template (written for basic_json, but +// accepting anything) is thus not called with a view, nor are the library's +// own conversions; the view converts such types through materialize(). +template +using serializer_from_json_t = decltype(Serializer::from_json(std::declval(), std::declval())); + +template +struct has_view_from_json + : conjunction < is_detected, + negation>> {}; + +// the base part of a type of the NLOHMANN_DEFINE_DERIVED_TYPE_* macros: from a +// view through its get_to(), which reads the base from the view where the +// base's from_json() accepts views, and from materialize() otherwise +template::value, int> = 0> +void from_json_base(const BasicJsonType& j, T& base) +{ + from_json_fn {}(j, base); +} + +template::value, int> = 0> +void from_json_base(const View& v, T& base) +{ + v.get_to(base); +} + +// the context of an exception thrown by a from_json() of the macros: the value +// itself for basic_json (for JSON_DIAGNOSTICS), none for a view +template::value, int> = 0> +const BasicJsonType * from_json_context(const BasicJsonType& j) noexcept +{ + return &j; +} + +template::value, int> = 0> +std::nullptr_t from_json_context(const View& /*unused*/) noexcept +{ + return nullptr; +} + +// a value as an exception message shows it (invalid UTF-8 replaced; the +// strings of a view are always valid UTF-8) +template::value, int> = 0> +typename BasicJsonType::string_t from_json_dump(const BasicJsonType& j) +{ + return j.dump(-1, ' ', false, error_handler_t::replace); +} + +template::value, int> = 0> +typename View::string_t from_json_dump(const View& v) +{ + return v.dump(); +} } // namespace detail diff --git a/include/nlohmann/detail/macro_scope.hpp b/include/nlohmann/detail/macro_scope.hpp index 061c48853..4f89f3463 100644 --- a/include/nlohmann/detail/macro_scope.hpp +++ b/include/nlohmann/detail/macro_scope.hpp @@ -291,15 +291,17 @@ }); \ j = ((it != std::end(m)) ? it : std::begin(m))->second; \ } \ - template \ + template::value, int> = 0> \ inline void from_json(const BasicJsonType& j, ENUM_TYPE& e) \ { \ /* NOLINTNEXTLINE(modernize-type-traits) we use C++11 */ \ static_assert(std::is_enum::value, #ENUM_TYPE " must be an enum!"); \ + /* (for a view, the table holds basic_json values, which compare with the view) */ \ + using nlohmann_json_value_t = nlohmann::detail::json_type_of_t; \ /* NOLINTNEXTLINE(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) we don't want to depend on */ \ - static const std::pair m[] = __VA_ARGS__; \ + static const std::pair m[] = __VA_ARGS__; \ auto it = std::find_if(std::begin(m), std::end(m), \ - [&j](const std::pair& ej_pair) -> bool \ + [&j](const std::pair& ej_pair) -> bool \ { \ return ej_pair.second == j; \ }); \ @@ -330,20 +332,22 @@ if (it != std::end(m)) j = it->second; \ else ::nlohmann::detail::templated_json_throw(nlohmann::detail::out_of_range::create(nlohmann::detail::exception_id::value_out_of_range,"enum value out of range for " #ENUM_TYPE, nullptr)); \ } \ - template \ + template::value, int> = 0> \ inline void from_json(const BasicJsonType& j, ENUM_TYPE& e) \ { \ /* NOLINTNEXTLINE(modernize-type-traits) we use C++11 */ \ static_assert(std::is_enum::value, #ENUM_TYPE " must be an enum!"); \ + /* (for a view, the table holds basic_json values, which compare with the view) */ \ + using nlohmann_json_value_t = nlohmann::detail::json_type_of_t; \ /* NOLINTNEXTLINE(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) we don't want to depend on */ \ - static const std::pair m[] = __VA_ARGS__; \ + static const std::pair m[] = __VA_ARGS__; \ auto it = std::find_if(std::begin(m), std::end(m), \ - [&j](const std::pair& ej_pair) -> bool \ + [&j](const std::pair& ej_pair) -> bool \ { \ return ej_pair.second == j; \ }); \ if (it != std::end(m)) e = it->first; \ - else ::nlohmann::detail::templated_json_throw(nlohmann::detail::out_of_range::create(nlohmann::detail::exception_id::value_out_of_range, nlohmann::detail::concat("enum value out of range for " #ENUM_TYPE ": ", j.dump(-1, ' ', false, nlohmann::detail::error_handler_t::replace)), &j)); \ + else ::nlohmann::detail::templated_json_throw(nlohmann::detail::out_of_range::create(nlohmann::detail::exception_id::value_out_of_range, nlohmann::detail::concat("enum value out of range for " #ENUM_TYPE ": ", nlohmann::detail::from_json_dump(j)), nlohmann::detail::from_json_context(j))); \ } // Ugly macros to avoid uglier copy-paste when specializing basic_json. They @@ -603,6 +607,8 @@ #define NLOHMANN_JSON_FROM_WITH_DEFAULT_WITH_NAME(v1, v2) nlohmann_json_t.v2 = !nlohmann_json_j.is_null() ? nlohmann_json_j.value(v1, nlohmann_json_default_obj.v2) : nlohmann_json_default_obj.v2; #define NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(...) template::value, int> = 0> __VA_ARGS__ +// from_json() also reads from the views of +#define NLOHMANN_JSON_FROM_TYPE_TEMPLATE(...) template::value, int> = 0> __VA_ARGS__ // Helpers used to dispatch the NLOHMANN_DEFINE_TYPE_*/NLOHMANN_DEFINE_DERIVED_TYPE_* // macros below between a zero-member and a one-or-more-member implementation @@ -655,18 +661,18 @@ */ #define NLOHMANN_JSON_DEFINE_TYPE_INTRUSIVE_MEMBERS(Type, ...) \ NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(friend void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) }) \ - NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(friend void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM, __VA_ARGS__)) }) + NLOHMANN_JSON_FROM_TYPE_TEMPLATE(friend void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM, __VA_ARGS__)) }) #define NLOHMANN_JSON_DEFINE_TYPE_INTRUSIVE_EMPTY(Type) \ NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(friend void to_json(BasicJsonType& nlohmann_json_j, const Type&) { nlohmann_json_j = BasicJsonType::object(); }) \ /* NOLINTNEXTLINE(bugprone-macro-parentheses) Type is used as a declarator type, not in an expression */ \ - NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(friend void from_json(const BasicJsonType&, Type&) noexcept { }) + NLOHMANN_JSON_FROM_TYPE_TEMPLATE(friend void from_json(const BasicJsonType&, Type&) noexcept { }) #define NLOHMANN_DEFINE_TYPE_INTRUSIVE(...) NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_TYPE_BODY(NLOHMANN_JSON_DEFINE_TYPE_INTRUSIVE_, __VA_ARGS__)(__VA_ARGS__)) #define NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_NAMES(Type, ...) \ NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(friend void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_DOUBLE_PASTE(NLOHMANN_JSON_TO_WITH_NAME, __VA_ARGS__)) }) \ - NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(friend void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_DOUBLE_PASTE(NLOHMANN_JSON_FROM_WITH_NAME, __VA_ARGS__)) }) + NLOHMANN_JSON_FROM_TYPE_TEMPLATE(friend void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_DOUBLE_PASTE(NLOHMANN_JSON_FROM_WITH_NAME, __VA_ARGS__)) }) /*! @brief macro @@ -676,7 +682,7 @@ */ #define NLOHMANN_JSON_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT_MEMBERS(Type, ...) \ NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(friend void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) }) \ - NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(friend void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { const Type nlohmann_json_default_obj{}; NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM_WITH_DEFAULT, __VA_ARGS__)) }) + NLOHMANN_JSON_FROM_TYPE_TEMPLATE(friend void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { const Type nlohmann_json_default_obj{}; NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM_WITH_DEFAULT, __VA_ARGS__)) }) // identical to NLOHMANN_JSON_DEFINE_TYPE_INTRUSIVE_EMPTY: with no members there is nothing to default #define NLOHMANN_JSON_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT_EMPTY(Type) NLOHMANN_JSON_DEFINE_TYPE_INTRUSIVE_EMPTY(Type) @@ -685,7 +691,7 @@ #define NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT_WITH_NAMES(Type, ...) \ NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(friend void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_DOUBLE_PASTE(NLOHMANN_JSON_TO_WITH_NAME, __VA_ARGS__)) }) \ - NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(friend void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { const Type nlohmann_json_default_obj{}; NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_DOUBLE_PASTE(NLOHMANN_JSON_FROM_WITH_DEFAULT_WITH_NAME, __VA_ARGS__)) }) + NLOHMANN_JSON_FROM_TYPE_TEMPLATE(friend void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { const Type nlohmann_json_default_obj{}; NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_DOUBLE_PASTE(NLOHMANN_JSON_FROM_WITH_DEFAULT_WITH_NAME, __VA_ARGS__)) }) /*! @brief macro @@ -712,18 +718,18 @@ */ #define NLOHMANN_JSON_DEFINE_TYPE_NON_INTRUSIVE_MEMBERS(Type, ...) \ NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) }) \ - NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM, __VA_ARGS__)) }) + NLOHMANN_JSON_FROM_TYPE_TEMPLATE(void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM, __VA_ARGS__)) }) #define NLOHMANN_JSON_DEFINE_TYPE_NON_INTRUSIVE_EMPTY(Type) \ NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(void to_json(BasicJsonType& nlohmann_json_j, const Type&) { nlohmann_json_j = BasicJsonType::object(); }) \ /* NOLINTNEXTLINE(bugprone-macro-parentheses) Type is used as a declarator type, not in an expression */ \ - NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(void from_json(const BasicJsonType&, Type&) noexcept { }) + NLOHMANN_JSON_FROM_TYPE_TEMPLATE(void from_json(const BasicJsonType&, Type&) noexcept { }) #define NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(...) NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_TYPE_BODY(NLOHMANN_JSON_DEFINE_TYPE_NON_INTRUSIVE_, __VA_ARGS__)(__VA_ARGS__)) #define NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_NAMES(Type, ...) \ NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_DOUBLE_PASTE(NLOHMANN_JSON_TO_WITH_NAME, __VA_ARGS__)) }) \ - NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_DOUBLE_PASTE(NLOHMANN_JSON_FROM_WITH_NAME, __VA_ARGS__)) }) + NLOHMANN_JSON_FROM_TYPE_TEMPLATE(void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_DOUBLE_PASTE(NLOHMANN_JSON_FROM_WITH_NAME, __VA_ARGS__)) }) /*! @brief macro @@ -733,7 +739,7 @@ */ #define NLOHMANN_JSON_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT_MEMBERS(Type, ...) \ NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) }) \ - NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { const Type nlohmann_json_default_obj{}; NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM_WITH_DEFAULT, __VA_ARGS__)) }) + NLOHMANN_JSON_FROM_TYPE_TEMPLATE(void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { const Type nlohmann_json_default_obj{}; NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM_WITH_DEFAULT, __VA_ARGS__)) }) // identical to NLOHMANN_JSON_DEFINE_TYPE_NON_INTRUSIVE_EMPTY: with no members there is nothing to default #define NLOHMANN_JSON_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT_EMPTY(Type) NLOHMANN_JSON_DEFINE_TYPE_NON_INTRUSIVE_EMPTY(Type) @@ -742,7 +748,7 @@ #define NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT_WITH_NAMES(Type, ...) \ NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_DOUBLE_PASTE(NLOHMANN_JSON_TO_WITH_NAME, __VA_ARGS__)) }) \ - NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { const Type nlohmann_json_default_obj{}; NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_DOUBLE_PASTE(NLOHMANN_JSON_FROM_WITH_DEFAULT_WITH_NAME, __VA_ARGS__)) }) + NLOHMANN_JSON_FROM_TYPE_TEMPLATE(void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { const Type nlohmann_json_default_obj{}; NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_DOUBLE_PASTE(NLOHMANN_JSON_FROM_WITH_DEFAULT_WITH_NAME, __VA_ARGS__)) }) /*! @brief macro @@ -769,18 +775,18 @@ */ #define NLOHMANN_JSON_DEFINE_DERIVED_TYPE_INTRUSIVE_MEMBERS(Type, BaseType, ...) \ NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(friend void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { nlohmann::to_json(nlohmann_json_j, static_cast(nlohmann_json_t)); NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) }) \ - NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(friend void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { nlohmann::from_json(nlohmann_json_j, static_cast(nlohmann_json_t)); NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM, __VA_ARGS__)) }) + NLOHMANN_JSON_FROM_TYPE_TEMPLATE(friend void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { ::nlohmann::detail::from_json_base(nlohmann_json_j, static_cast(nlohmann_json_t)); NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM, __VA_ARGS__)) }) #define NLOHMANN_JSON_DEFINE_DERIVED_TYPE_INTRUSIVE_EMPTY(Type, BaseType) \ NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(friend void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { nlohmann::to_json(nlohmann_json_j, static_cast(nlohmann_json_t)); }) \ /* NOLINTNEXTLINE(bugprone-macro-parentheses) Type/BaseType are used as declarator types, not in expressions */ \ - NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(friend void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { nlohmann::from_json(nlohmann_json_j, static_cast(nlohmann_json_t)); }) + NLOHMANN_JSON_FROM_TYPE_TEMPLATE(friend void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { ::nlohmann::detail::from_json_base(nlohmann_json_j, static_cast(nlohmann_json_t)); }) #define NLOHMANN_DEFINE_DERIVED_TYPE_INTRUSIVE(...) NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_DERIVED_TYPE_BODY(NLOHMANN_JSON_DEFINE_DERIVED_TYPE_INTRUSIVE_, __VA_ARGS__)(__VA_ARGS__)) #define NLOHMANN_DEFINE_DERIVED_TYPE_INTRUSIVE_WITH_NAMES(Type, BaseType, ...) \ NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(friend void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { nlohmann::to_json(nlohmann_json_j, static_cast(nlohmann_json_t)); NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_DOUBLE_PASTE(NLOHMANN_JSON_TO_WITH_NAME, __VA_ARGS__)) }) \ - NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(friend void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { nlohmann::from_json(nlohmann_json_j, static_cast(nlohmann_json_t)); NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_DOUBLE_PASTE(NLOHMANN_JSON_FROM_WITH_NAME, __VA_ARGS__)) }) + NLOHMANN_JSON_FROM_TYPE_TEMPLATE(friend void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { ::nlohmann::detail::from_json_base(nlohmann_json_j, static_cast(nlohmann_json_t)); NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_DOUBLE_PASTE(NLOHMANN_JSON_FROM_WITH_NAME, __VA_ARGS__)) }) /*! @brief macro @@ -790,7 +796,7 @@ */ #define NLOHMANN_JSON_DEFINE_DERIVED_TYPE_INTRUSIVE_WITH_DEFAULT_MEMBERS(Type, BaseType, ...) \ NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(friend void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { nlohmann::to_json(nlohmann_json_j, static_cast(nlohmann_json_t)); NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) }) \ - NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(friend void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { nlohmann::from_json(nlohmann_json_j, static_cast(nlohmann_json_t)); const Type nlohmann_json_default_obj{}; NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM_WITH_DEFAULT, __VA_ARGS__)) }) + NLOHMANN_JSON_FROM_TYPE_TEMPLATE(friend void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { ::nlohmann::detail::from_json_base(nlohmann_json_j, static_cast(nlohmann_json_t)); const Type nlohmann_json_default_obj{}; NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM_WITH_DEFAULT, __VA_ARGS__)) }) // identical to NLOHMANN_JSON_DEFINE_DERIVED_TYPE_INTRUSIVE_EMPTY: with no members there is nothing to default #define NLOHMANN_JSON_DEFINE_DERIVED_TYPE_INTRUSIVE_WITH_DEFAULT_EMPTY(Type, BaseType) NLOHMANN_JSON_DEFINE_DERIVED_TYPE_INTRUSIVE_EMPTY(Type, BaseType) @@ -799,7 +805,7 @@ #define NLOHMANN_DEFINE_DERIVED_TYPE_INTRUSIVE_WITH_DEFAULT_WITH_NAMES(Type, BaseType, ...) \ NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(friend void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { nlohmann::to_json(nlohmann_json_j, static_cast(nlohmann_json_t)); NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_DOUBLE_PASTE(NLOHMANN_JSON_TO_WITH_NAME, __VA_ARGS__)) }) \ - NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(friend void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { nlohmann::from_json(nlohmann_json_j, static_cast(nlohmann_json_t)); const Type nlohmann_json_default_obj{}; NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_DOUBLE_PASTE(NLOHMANN_JSON_FROM_WITH_DEFAULT_WITH_NAME, __VA_ARGS__)) }) + NLOHMANN_JSON_FROM_TYPE_TEMPLATE(friend void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { ::nlohmann::detail::from_json_base(nlohmann_json_j, static_cast(nlohmann_json_t)); const Type nlohmann_json_default_obj{}; NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_DOUBLE_PASTE(NLOHMANN_JSON_FROM_WITH_DEFAULT_WITH_NAME, __VA_ARGS__)) }) /*! @brief macro @@ -827,18 +833,18 @@ */ #define NLOHMANN_JSON_DEFINE_DERIVED_TYPE_NON_INTRUSIVE_MEMBERS(Type, BaseType, ...) \ NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { nlohmann::to_json(nlohmann_json_j, static_cast(nlohmann_json_t)); NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) }) \ - NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { nlohmann::from_json(nlohmann_json_j, static_cast(nlohmann_json_t)); NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM, __VA_ARGS__)) }) + NLOHMANN_JSON_FROM_TYPE_TEMPLATE(void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { ::nlohmann::detail::from_json_base(nlohmann_json_j, static_cast(nlohmann_json_t)); NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM, __VA_ARGS__)) }) #define NLOHMANN_JSON_DEFINE_DERIVED_TYPE_NON_INTRUSIVE_EMPTY(Type, BaseType) \ NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { nlohmann::to_json(nlohmann_json_j, static_cast(nlohmann_json_t)); }) \ /* NOLINTNEXTLINE(bugprone-macro-parentheses) Type/BaseType are used as declarator types, not in expressions */ \ - NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { nlohmann::from_json(nlohmann_json_j, static_cast(nlohmann_json_t)); }) + NLOHMANN_JSON_FROM_TYPE_TEMPLATE(void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { ::nlohmann::detail::from_json_base(nlohmann_json_j, static_cast(nlohmann_json_t)); }) #define NLOHMANN_DEFINE_DERIVED_TYPE_NON_INTRUSIVE(...) NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_DERIVED_TYPE_BODY(NLOHMANN_JSON_DEFINE_DERIVED_TYPE_NON_INTRUSIVE_, __VA_ARGS__)(__VA_ARGS__)) #define NLOHMANN_DEFINE_DERIVED_TYPE_NON_INTRUSIVE_WITH_NAMES(Type, BaseType, ...) \ NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { nlohmann::to_json(nlohmann_json_j, static_cast(nlohmann_json_t)); NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_DOUBLE_PASTE(NLOHMANN_JSON_TO_WITH_NAME, __VA_ARGS__)) }) \ - NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { nlohmann::from_json(nlohmann_json_j, static_cast(nlohmann_json_t)); NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_DOUBLE_PASTE(NLOHMANN_JSON_FROM_WITH_NAME, __VA_ARGS__)) }) + NLOHMANN_JSON_FROM_TYPE_TEMPLATE(void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { ::nlohmann::detail::from_json_base(nlohmann_json_j, static_cast(nlohmann_json_t)); NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_DOUBLE_PASTE(NLOHMANN_JSON_FROM_WITH_NAME, __VA_ARGS__)) }) /*! @brief macro @@ -848,7 +854,7 @@ */ #define NLOHMANN_JSON_DEFINE_DERIVED_TYPE_NON_INTRUSIVE_WITH_DEFAULT_MEMBERS(Type, BaseType, ...) \ NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { nlohmann::to_json(nlohmann_json_j, static_cast(nlohmann_json_t)); NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) }) \ - NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { nlohmann::from_json(nlohmann_json_j, static_cast(nlohmann_json_t)); const Type nlohmann_json_default_obj{}; NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM_WITH_DEFAULT, __VA_ARGS__)) }) + NLOHMANN_JSON_FROM_TYPE_TEMPLATE(void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { ::nlohmann::detail::from_json_base(nlohmann_json_j, static_cast(nlohmann_json_t)); const Type nlohmann_json_default_obj{}; NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM_WITH_DEFAULT, __VA_ARGS__)) }) // identical to NLOHMANN_JSON_DEFINE_DERIVED_TYPE_NON_INTRUSIVE_EMPTY: with no members there is nothing to default #define NLOHMANN_JSON_DEFINE_DERIVED_TYPE_NON_INTRUSIVE_WITH_DEFAULT_EMPTY(Type, BaseType) NLOHMANN_JSON_DEFINE_DERIVED_TYPE_NON_INTRUSIVE_EMPTY(Type, BaseType) @@ -857,7 +863,7 @@ #define NLOHMANN_DEFINE_DERIVED_TYPE_NON_INTRUSIVE_WITH_DEFAULT_WITH_NAMES(Type, BaseType, ...) \ NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { nlohmann::to_json(nlohmann_json_j, static_cast(nlohmann_json_t)); NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_DOUBLE_PASTE(NLOHMANN_JSON_TO_WITH_NAME, __VA_ARGS__)) }) \ - NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { nlohmann::from_json(nlohmann_json_j, static_cast(nlohmann_json_t)); const Type nlohmann_json_default_obj{}; NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_DOUBLE_PASTE(NLOHMANN_JSON_FROM_WITH_DEFAULT_WITH_NAME, __VA_ARGS__)) }) + NLOHMANN_JSON_FROM_TYPE_TEMPLATE(void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { ::nlohmann::detail::from_json_base(nlohmann_json_j, static_cast(nlohmann_json_t)); const Type nlohmann_json_default_obj{}; NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_DOUBLE_PASTE(NLOHMANN_JSON_FROM_WITH_DEFAULT_WITH_NAME, __VA_ARGS__)) }) /*! @brief macro diff --git a/include/nlohmann/detail/meta/type_traits.hpp b/include/nlohmann/detail/meta/type_traits.hpp index 387691b6b..bd72c3a65 100644 --- a/include/nlohmann/detail/meta/type_traits.hpp +++ b/include/nlohmann/detail/meta/type_traits.hpp @@ -340,6 +340,47 @@ template struct negation : std::integral_constant < bool, !B::value > { #endif +/////////////////////// +// json_view helpers // +/////////////////////// + +template +using basic_json_type_t = typename T::basic_json_type; + +template +using materialize_function_t = decltype(std::declval().materialize()); + +// a basic_json_view of (detected by its member type +// basic_json_type, a basic_json, and materialize() returning that type, so that +// json.hpp needs no declaration of the view) +template +struct is_json_view : std::false_type {}; + +template +struct is_json_view>::value>> + : std::is_same, basic_json_type_t> {}; + +// the types from_json() functions of the NLOHMANN_DEFINE_TYPE_* and +// NLOHMANN_JSON_SERIALIZE_ENUM macros read from +template +struct is_basic_json_or_view : disjunction, is_json_view> {}; + +// the basic_json type of a basic_json value or of a view +template +struct json_type_of +{ + using type = T; +}; + +template +struct json_type_of::value>> +{ + using type = typename T::basic_json_type; +}; + +template +using json_type_of_t = typename json_type_of::type; + // Reimplementation of is_constructible and is_default_constructible, due to them being broken for // std::pair and std::tuple until LWG 2367 fix (see https://cplusplus.github.io/LWG/lwg-defects.html#2367). // This causes compile errors in e.g., Clang 3.5 or GCC 4.9. diff --git a/include/nlohmann/json_view.hpp b/include/nlohmann/json_view.hpp index 79c9b8dde..ebcb13733 100644 --- a/include/nlohmann/json_view.hpp +++ b/include/nlohmann/json_view.hpp @@ -106,6 +106,8 @@ class basic_json_view using navigation = detail::view::navigation; public: + /// the basic_json type of the document (it also marks views for is_json_view) + using basic_json_type = BasicJsonType; using value_t = detail::value_t; using string_t = typename BasicJsonType::string_t; using number_integer_t = typename BasicJsonType::number_integer_t; @@ -576,8 +578,10 @@ class basic_json_view /// the value converted to T, as BasicJsonType::get(): arithmetic types, /// strings (string_view_t without a copy), std::nullptr_t, std::vector, - /// maps with string keys, and views are converted directly; other types - /// through materialize().get() + /// maps with string keys, and views are converted directly; types whose + /// from_json() accepts views (those of the NLOHMANN_DEFINE_TYPE_* and + /// NLOHMANN_JSON_SERIALIZE_ENUM macros, say) read the view; other types + /// go through materialize().get() template NLOHMANN_VIEW_ALWAYS_INLINE T get() const { @@ -585,11 +589,12 @@ class basic_json_view } /// as BasicJsonType::get_to(): the types get() converts directly are - /// assigned, all others are passed to from_json with v as it is + /// assigned, all others are passed to from_json with v as it is (with + /// the view if from_json() accepts views, else with materialize()) template T& get_to(T& v) const { - get_to_impl(v, detail::view::is_direct_value {}); + get_to_impl(v, get_to_kind {}); return v; } @@ -932,6 +937,26 @@ class basic_json_view return detail::view::map_value>(*this); } + /// whether the from_json() of T's serializer reads views (see + /// detail::has_view_from_json) + template + using reads_view = detail::has_view_from_json, basic_json_view, T>; + + /// how get_to() converts: 2 directly, 1 with a from_json() that reads + /// views, 0 through materialize() + template + using get_to_kind = std::integral_constant < int, detail::view::is_direct_value::value + ? 2 : static_cast(reads_view::value) >; + + /// types whose from_json() reads views, without materialize() + template < typename T, typename std::enable_if < reads_view::value && std::is_default_constructible::value, int >::type = 0 > + T get_impl(detail::view::value_tag /*unused*/, detail::priority_tag<1> /*unused*/) const + { + auto v = T(); + BasicJsonType::template json_serializer::from_json(*this, v); + return v; + } + /// everything else through the BasicJsonType value (from_json included) template T get_impl(detail::view::value_tag /*unused*/, detail::priority_tag<0> /*unused*/) const @@ -942,14 +967,20 @@ class basic_json_view // --- get_to() dispatch --- template - void get_to_impl(T& v, std::true_type /*direct*/) const + void get_to_impl(T& v, std::integral_constant /*direct*/) const { v = get(); } /// from_json may keep parts of v (members a JSON object lacks, say) template - void get_to_impl(T& v, std::false_type /*direct*/) const + void get_to_impl(T& v, std::integral_constant /*from_json reads views*/) const + { + BasicJsonType::template json_serializer::from_json(*this, v); + } + + template + void get_to_impl(T& v, std::integral_constant /*through materialize()*/) const { materialize().get_to(v); } @@ -1654,6 +1685,15 @@ class basic_json_document std::unique_ptr m_data{}; // NOLINT(readability-redundant-member-init) }; +/*! +@brief whether T is a basic_json_view (of any BasicJsonType and view_mode) + +For constraining templates that accept views, e.g. a from_json() written for +both basic_json values and views. +*/ +template +struct is_json_view : detail::is_json_view {}; + /// a parsed JSON text for json using json_document = basic_json_document; /// a value of a json_document diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index b6281565f..b0492ec78 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -272,6 +272,7 @@ #include // min, transform #include // array +#include // nullptr_t #include // forward_list #include // inserter, front_inserter, end #include // map @@ -2663,15 +2664,17 @@ JSON_HEDLEY_DIAGNOSTIC_POP }); \ j = ((it != std::end(m)) ? it : std::begin(m))->second; \ } \ - template \ + template::value, int> = 0> \ inline void from_json(const BasicJsonType& j, ENUM_TYPE& e) \ { \ /* NOLINTNEXTLINE(modernize-type-traits) we use C++11 */ \ static_assert(std::is_enum::value, #ENUM_TYPE " must be an enum!"); \ + /* (for a view, the table holds basic_json values, which compare with the view) */ \ + using nlohmann_json_value_t = nlohmann::detail::json_type_of_t; \ /* NOLINTNEXTLINE(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) we don't want to depend on */ \ - static const std::pair m[] = __VA_ARGS__; \ + static const std::pair m[] = __VA_ARGS__; \ auto it = std::find_if(std::begin(m), std::end(m), \ - [&j](const std::pair& ej_pair) -> bool \ + [&j](const std::pair& ej_pair) -> bool \ { \ return ej_pair.second == j; \ }); \ @@ -2702,20 +2705,22 @@ JSON_HEDLEY_DIAGNOSTIC_POP if (it != std::end(m)) j = it->second; \ else ::nlohmann::detail::templated_json_throw(nlohmann::detail::out_of_range::create(nlohmann::detail::exception_id::value_out_of_range,"enum value out of range for " #ENUM_TYPE, nullptr)); \ } \ - template \ + template::value, int> = 0> \ inline void from_json(const BasicJsonType& j, ENUM_TYPE& e) \ { \ /* NOLINTNEXTLINE(modernize-type-traits) we use C++11 */ \ static_assert(std::is_enum::value, #ENUM_TYPE " must be an enum!"); \ + /* (for a view, the table holds basic_json values, which compare with the view) */ \ + using nlohmann_json_value_t = nlohmann::detail::json_type_of_t; \ /* NOLINTNEXTLINE(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) we don't want to depend on */ \ - static const std::pair m[] = __VA_ARGS__; \ + static const std::pair m[] = __VA_ARGS__; \ auto it = std::find_if(std::begin(m), std::end(m), \ - [&j](const std::pair& ej_pair) -> bool \ + [&j](const std::pair& ej_pair) -> bool \ { \ return ej_pair.second == j; \ }); \ if (it != std::end(m)) e = it->first; \ - else ::nlohmann::detail::templated_json_throw(nlohmann::detail::out_of_range::create(nlohmann::detail::exception_id::value_out_of_range, nlohmann::detail::concat("enum value out of range for " #ENUM_TYPE ": ", j.dump(-1, ' ', false, nlohmann::detail::error_handler_t::replace)), &j)); \ + else ::nlohmann::detail::templated_json_throw(nlohmann::detail::out_of_range::create(nlohmann::detail::exception_id::value_out_of_range, nlohmann::detail::concat("enum value out of range for " #ENUM_TYPE ": ", nlohmann::detail::from_json_dump(j)), nlohmann::detail::from_json_context(j))); \ } // Ugly macros to avoid uglier copy-paste when specializing basic_json. They @@ -2975,6 +2980,8 @@ JSON_HEDLEY_DIAGNOSTIC_POP #define NLOHMANN_JSON_FROM_WITH_DEFAULT_WITH_NAME(v1, v2) nlohmann_json_t.v2 = !nlohmann_json_j.is_null() ? nlohmann_json_j.value(v1, nlohmann_json_default_obj.v2) : nlohmann_json_default_obj.v2; #define NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(...) template::value, int> = 0> __VA_ARGS__ +// from_json() also reads from the views of +#define NLOHMANN_JSON_FROM_TYPE_TEMPLATE(...) template::value, int> = 0> __VA_ARGS__ // Helpers used to dispatch the NLOHMANN_DEFINE_TYPE_*/NLOHMANN_DEFINE_DERIVED_TYPE_* // macros below between a zero-member and a one-or-more-member implementation @@ -3027,18 +3034,18 @@ JSON_HEDLEY_DIAGNOSTIC_POP */ #define NLOHMANN_JSON_DEFINE_TYPE_INTRUSIVE_MEMBERS(Type, ...) \ NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(friend void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) }) \ - NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(friend void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM, __VA_ARGS__)) }) + NLOHMANN_JSON_FROM_TYPE_TEMPLATE(friend void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM, __VA_ARGS__)) }) #define NLOHMANN_JSON_DEFINE_TYPE_INTRUSIVE_EMPTY(Type) \ NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(friend void to_json(BasicJsonType& nlohmann_json_j, const Type&) { nlohmann_json_j = BasicJsonType::object(); }) \ /* NOLINTNEXTLINE(bugprone-macro-parentheses) Type is used as a declarator type, not in an expression */ \ - NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(friend void from_json(const BasicJsonType&, Type&) noexcept { }) + NLOHMANN_JSON_FROM_TYPE_TEMPLATE(friend void from_json(const BasicJsonType&, Type&) noexcept { }) #define NLOHMANN_DEFINE_TYPE_INTRUSIVE(...) NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_TYPE_BODY(NLOHMANN_JSON_DEFINE_TYPE_INTRUSIVE_, __VA_ARGS__)(__VA_ARGS__)) #define NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_NAMES(Type, ...) \ NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(friend void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_DOUBLE_PASTE(NLOHMANN_JSON_TO_WITH_NAME, __VA_ARGS__)) }) \ - NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(friend void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_DOUBLE_PASTE(NLOHMANN_JSON_FROM_WITH_NAME, __VA_ARGS__)) }) + NLOHMANN_JSON_FROM_TYPE_TEMPLATE(friend void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_DOUBLE_PASTE(NLOHMANN_JSON_FROM_WITH_NAME, __VA_ARGS__)) }) /*! @brief macro @@ -3048,7 +3055,7 @@ JSON_HEDLEY_DIAGNOSTIC_POP */ #define NLOHMANN_JSON_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT_MEMBERS(Type, ...) \ NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(friend void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) }) \ - NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(friend void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { const Type nlohmann_json_default_obj{}; NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM_WITH_DEFAULT, __VA_ARGS__)) }) + NLOHMANN_JSON_FROM_TYPE_TEMPLATE(friend void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { const Type nlohmann_json_default_obj{}; NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM_WITH_DEFAULT, __VA_ARGS__)) }) // identical to NLOHMANN_JSON_DEFINE_TYPE_INTRUSIVE_EMPTY: with no members there is nothing to default #define NLOHMANN_JSON_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT_EMPTY(Type) NLOHMANN_JSON_DEFINE_TYPE_INTRUSIVE_EMPTY(Type) @@ -3057,7 +3064,7 @@ JSON_HEDLEY_DIAGNOSTIC_POP #define NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT_WITH_NAMES(Type, ...) \ NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(friend void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_DOUBLE_PASTE(NLOHMANN_JSON_TO_WITH_NAME, __VA_ARGS__)) }) \ - NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(friend void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { const Type nlohmann_json_default_obj{}; NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_DOUBLE_PASTE(NLOHMANN_JSON_FROM_WITH_DEFAULT_WITH_NAME, __VA_ARGS__)) }) + NLOHMANN_JSON_FROM_TYPE_TEMPLATE(friend void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { const Type nlohmann_json_default_obj{}; NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_DOUBLE_PASTE(NLOHMANN_JSON_FROM_WITH_DEFAULT_WITH_NAME, __VA_ARGS__)) }) /*! @brief macro @@ -3084,18 +3091,18 @@ JSON_HEDLEY_DIAGNOSTIC_POP */ #define NLOHMANN_JSON_DEFINE_TYPE_NON_INTRUSIVE_MEMBERS(Type, ...) \ NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) }) \ - NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM, __VA_ARGS__)) }) + NLOHMANN_JSON_FROM_TYPE_TEMPLATE(void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM, __VA_ARGS__)) }) #define NLOHMANN_JSON_DEFINE_TYPE_NON_INTRUSIVE_EMPTY(Type) \ NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(void to_json(BasicJsonType& nlohmann_json_j, const Type&) { nlohmann_json_j = BasicJsonType::object(); }) \ /* NOLINTNEXTLINE(bugprone-macro-parentheses) Type is used as a declarator type, not in an expression */ \ - NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(void from_json(const BasicJsonType&, Type&) noexcept { }) + NLOHMANN_JSON_FROM_TYPE_TEMPLATE(void from_json(const BasicJsonType&, Type&) noexcept { }) #define NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(...) NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_TYPE_BODY(NLOHMANN_JSON_DEFINE_TYPE_NON_INTRUSIVE_, __VA_ARGS__)(__VA_ARGS__)) #define NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_NAMES(Type, ...) \ NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_DOUBLE_PASTE(NLOHMANN_JSON_TO_WITH_NAME, __VA_ARGS__)) }) \ - NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_DOUBLE_PASTE(NLOHMANN_JSON_FROM_WITH_NAME, __VA_ARGS__)) }) + NLOHMANN_JSON_FROM_TYPE_TEMPLATE(void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_DOUBLE_PASTE(NLOHMANN_JSON_FROM_WITH_NAME, __VA_ARGS__)) }) /*! @brief macro @@ -3105,7 +3112,7 @@ JSON_HEDLEY_DIAGNOSTIC_POP */ #define NLOHMANN_JSON_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT_MEMBERS(Type, ...) \ NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) }) \ - NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { const Type nlohmann_json_default_obj{}; NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM_WITH_DEFAULT, __VA_ARGS__)) }) + NLOHMANN_JSON_FROM_TYPE_TEMPLATE(void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { const Type nlohmann_json_default_obj{}; NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM_WITH_DEFAULT, __VA_ARGS__)) }) // identical to NLOHMANN_JSON_DEFINE_TYPE_NON_INTRUSIVE_EMPTY: with no members there is nothing to default #define NLOHMANN_JSON_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT_EMPTY(Type) NLOHMANN_JSON_DEFINE_TYPE_NON_INTRUSIVE_EMPTY(Type) @@ -3114,7 +3121,7 @@ JSON_HEDLEY_DIAGNOSTIC_POP #define NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT_WITH_NAMES(Type, ...) \ NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_DOUBLE_PASTE(NLOHMANN_JSON_TO_WITH_NAME, __VA_ARGS__)) }) \ - NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { const Type nlohmann_json_default_obj{}; NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_DOUBLE_PASTE(NLOHMANN_JSON_FROM_WITH_DEFAULT_WITH_NAME, __VA_ARGS__)) }) + NLOHMANN_JSON_FROM_TYPE_TEMPLATE(void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { const Type nlohmann_json_default_obj{}; NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_DOUBLE_PASTE(NLOHMANN_JSON_FROM_WITH_DEFAULT_WITH_NAME, __VA_ARGS__)) }) /*! @brief macro @@ -3141,18 +3148,18 @@ JSON_HEDLEY_DIAGNOSTIC_POP */ #define NLOHMANN_JSON_DEFINE_DERIVED_TYPE_INTRUSIVE_MEMBERS(Type, BaseType, ...) \ NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(friend void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { nlohmann::to_json(nlohmann_json_j, static_cast(nlohmann_json_t)); NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) }) \ - NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(friend void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { nlohmann::from_json(nlohmann_json_j, static_cast(nlohmann_json_t)); NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM, __VA_ARGS__)) }) + NLOHMANN_JSON_FROM_TYPE_TEMPLATE(friend void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { ::nlohmann::detail::from_json_base(nlohmann_json_j, static_cast(nlohmann_json_t)); NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM, __VA_ARGS__)) }) #define NLOHMANN_JSON_DEFINE_DERIVED_TYPE_INTRUSIVE_EMPTY(Type, BaseType) \ NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(friend void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { nlohmann::to_json(nlohmann_json_j, static_cast(nlohmann_json_t)); }) \ /* NOLINTNEXTLINE(bugprone-macro-parentheses) Type/BaseType are used as declarator types, not in expressions */ \ - NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(friend void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { nlohmann::from_json(nlohmann_json_j, static_cast(nlohmann_json_t)); }) + NLOHMANN_JSON_FROM_TYPE_TEMPLATE(friend void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { ::nlohmann::detail::from_json_base(nlohmann_json_j, static_cast(nlohmann_json_t)); }) #define NLOHMANN_DEFINE_DERIVED_TYPE_INTRUSIVE(...) NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_DERIVED_TYPE_BODY(NLOHMANN_JSON_DEFINE_DERIVED_TYPE_INTRUSIVE_, __VA_ARGS__)(__VA_ARGS__)) #define NLOHMANN_DEFINE_DERIVED_TYPE_INTRUSIVE_WITH_NAMES(Type, BaseType, ...) \ NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(friend void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { nlohmann::to_json(nlohmann_json_j, static_cast(nlohmann_json_t)); NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_DOUBLE_PASTE(NLOHMANN_JSON_TO_WITH_NAME, __VA_ARGS__)) }) \ - NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(friend void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { nlohmann::from_json(nlohmann_json_j, static_cast(nlohmann_json_t)); NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_DOUBLE_PASTE(NLOHMANN_JSON_FROM_WITH_NAME, __VA_ARGS__)) }) + NLOHMANN_JSON_FROM_TYPE_TEMPLATE(friend void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { ::nlohmann::detail::from_json_base(nlohmann_json_j, static_cast(nlohmann_json_t)); NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_DOUBLE_PASTE(NLOHMANN_JSON_FROM_WITH_NAME, __VA_ARGS__)) }) /*! @brief macro @@ -3162,7 +3169,7 @@ JSON_HEDLEY_DIAGNOSTIC_POP */ #define NLOHMANN_JSON_DEFINE_DERIVED_TYPE_INTRUSIVE_WITH_DEFAULT_MEMBERS(Type, BaseType, ...) \ NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(friend void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { nlohmann::to_json(nlohmann_json_j, static_cast(nlohmann_json_t)); NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) }) \ - NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(friend void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { nlohmann::from_json(nlohmann_json_j, static_cast(nlohmann_json_t)); const Type nlohmann_json_default_obj{}; NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM_WITH_DEFAULT, __VA_ARGS__)) }) + NLOHMANN_JSON_FROM_TYPE_TEMPLATE(friend void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { ::nlohmann::detail::from_json_base(nlohmann_json_j, static_cast(nlohmann_json_t)); const Type nlohmann_json_default_obj{}; NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM_WITH_DEFAULT, __VA_ARGS__)) }) // identical to NLOHMANN_JSON_DEFINE_DERIVED_TYPE_INTRUSIVE_EMPTY: with no members there is nothing to default #define NLOHMANN_JSON_DEFINE_DERIVED_TYPE_INTRUSIVE_WITH_DEFAULT_EMPTY(Type, BaseType) NLOHMANN_JSON_DEFINE_DERIVED_TYPE_INTRUSIVE_EMPTY(Type, BaseType) @@ -3171,7 +3178,7 @@ JSON_HEDLEY_DIAGNOSTIC_POP #define NLOHMANN_DEFINE_DERIVED_TYPE_INTRUSIVE_WITH_DEFAULT_WITH_NAMES(Type, BaseType, ...) \ NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(friend void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { nlohmann::to_json(nlohmann_json_j, static_cast(nlohmann_json_t)); NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_DOUBLE_PASTE(NLOHMANN_JSON_TO_WITH_NAME, __VA_ARGS__)) }) \ - NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(friend void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { nlohmann::from_json(nlohmann_json_j, static_cast(nlohmann_json_t)); const Type nlohmann_json_default_obj{}; NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_DOUBLE_PASTE(NLOHMANN_JSON_FROM_WITH_DEFAULT_WITH_NAME, __VA_ARGS__)) }) + NLOHMANN_JSON_FROM_TYPE_TEMPLATE(friend void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { ::nlohmann::detail::from_json_base(nlohmann_json_j, static_cast(nlohmann_json_t)); const Type nlohmann_json_default_obj{}; NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_DOUBLE_PASTE(NLOHMANN_JSON_FROM_WITH_DEFAULT_WITH_NAME, __VA_ARGS__)) }) /*! @brief macro @@ -3199,18 +3206,18 @@ JSON_HEDLEY_DIAGNOSTIC_POP */ #define NLOHMANN_JSON_DEFINE_DERIVED_TYPE_NON_INTRUSIVE_MEMBERS(Type, BaseType, ...) \ NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { nlohmann::to_json(nlohmann_json_j, static_cast(nlohmann_json_t)); NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) }) \ - NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { nlohmann::from_json(nlohmann_json_j, static_cast(nlohmann_json_t)); NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM, __VA_ARGS__)) }) + NLOHMANN_JSON_FROM_TYPE_TEMPLATE(void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { ::nlohmann::detail::from_json_base(nlohmann_json_j, static_cast(nlohmann_json_t)); NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM, __VA_ARGS__)) }) #define NLOHMANN_JSON_DEFINE_DERIVED_TYPE_NON_INTRUSIVE_EMPTY(Type, BaseType) \ NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { nlohmann::to_json(nlohmann_json_j, static_cast(nlohmann_json_t)); }) \ /* NOLINTNEXTLINE(bugprone-macro-parentheses) Type/BaseType are used as declarator types, not in expressions */ \ - NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { nlohmann::from_json(nlohmann_json_j, static_cast(nlohmann_json_t)); }) + NLOHMANN_JSON_FROM_TYPE_TEMPLATE(void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { ::nlohmann::detail::from_json_base(nlohmann_json_j, static_cast(nlohmann_json_t)); }) #define NLOHMANN_DEFINE_DERIVED_TYPE_NON_INTRUSIVE(...) NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_DERIVED_TYPE_BODY(NLOHMANN_JSON_DEFINE_DERIVED_TYPE_NON_INTRUSIVE_, __VA_ARGS__)(__VA_ARGS__)) #define NLOHMANN_DEFINE_DERIVED_TYPE_NON_INTRUSIVE_WITH_NAMES(Type, BaseType, ...) \ NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { nlohmann::to_json(nlohmann_json_j, static_cast(nlohmann_json_t)); NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_DOUBLE_PASTE(NLOHMANN_JSON_TO_WITH_NAME, __VA_ARGS__)) }) \ - NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { nlohmann::from_json(nlohmann_json_j, static_cast(nlohmann_json_t)); NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_DOUBLE_PASTE(NLOHMANN_JSON_FROM_WITH_NAME, __VA_ARGS__)) }) + NLOHMANN_JSON_FROM_TYPE_TEMPLATE(void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { ::nlohmann::detail::from_json_base(nlohmann_json_j, static_cast(nlohmann_json_t)); NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_DOUBLE_PASTE(NLOHMANN_JSON_FROM_WITH_NAME, __VA_ARGS__)) }) /*! @brief macro @@ -3220,7 +3227,7 @@ JSON_HEDLEY_DIAGNOSTIC_POP */ #define NLOHMANN_JSON_DEFINE_DERIVED_TYPE_NON_INTRUSIVE_WITH_DEFAULT_MEMBERS(Type, BaseType, ...) \ NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { nlohmann::to_json(nlohmann_json_j, static_cast(nlohmann_json_t)); NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) }) \ - NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { nlohmann::from_json(nlohmann_json_j, static_cast(nlohmann_json_t)); const Type nlohmann_json_default_obj{}; NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM_WITH_DEFAULT, __VA_ARGS__)) }) + NLOHMANN_JSON_FROM_TYPE_TEMPLATE(void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { ::nlohmann::detail::from_json_base(nlohmann_json_j, static_cast(nlohmann_json_t)); const Type nlohmann_json_default_obj{}; NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM_WITH_DEFAULT, __VA_ARGS__)) }) // identical to NLOHMANN_JSON_DEFINE_DERIVED_TYPE_NON_INTRUSIVE_EMPTY: with no members there is nothing to default #define NLOHMANN_JSON_DEFINE_DERIVED_TYPE_NON_INTRUSIVE_WITH_DEFAULT_EMPTY(Type, BaseType) NLOHMANN_JSON_DEFINE_DERIVED_TYPE_NON_INTRUSIVE_EMPTY(Type, BaseType) @@ -3229,7 +3236,7 @@ JSON_HEDLEY_DIAGNOSTIC_POP #define NLOHMANN_DEFINE_DERIVED_TYPE_NON_INTRUSIVE_WITH_DEFAULT_WITH_NAMES(Type, BaseType, ...) \ NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { nlohmann::to_json(nlohmann_json_j, static_cast(nlohmann_json_t)); NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_DOUBLE_PASTE(NLOHMANN_JSON_TO_WITH_NAME, __VA_ARGS__)) }) \ - NLOHMANN_JSON_BASIC_TYPE_TEMPLATE(void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { nlohmann::from_json(nlohmann_json_j, static_cast(nlohmann_json_t)); const Type nlohmann_json_default_obj{}; NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_DOUBLE_PASTE(NLOHMANN_JSON_FROM_WITH_DEFAULT_WITH_NAME, __VA_ARGS__)) }) + NLOHMANN_JSON_FROM_TYPE_TEMPLATE(void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { ::nlohmann::detail::from_json_base(nlohmann_json_j, static_cast(nlohmann_json_t)); const Type nlohmann_json_default_obj{}; NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_DOUBLE_PASTE(NLOHMANN_JSON_FROM_WITH_DEFAULT_WITH_NAME, __VA_ARGS__)) }) /*! @brief macro @@ -4516,6 +4523,47 @@ template struct negation : std::integral_constant < bool, !B::value > { #endif +/////////////////////// +// json_view helpers // +/////////////////////// + +template +using basic_json_type_t = typename T::basic_json_type; + +template +using materialize_function_t = decltype(std::declval().materialize()); + +// a basic_json_view of (detected by its member type +// basic_json_type, a basic_json, and materialize() returning that type, so that +// json.hpp needs no declaration of the view) +template +struct is_json_view : std::false_type {}; + +template +struct is_json_view>::value>> + : std::is_same, basic_json_type_t> {}; + +// the types from_json() functions of the NLOHMANN_DEFINE_TYPE_* and +// NLOHMANN_JSON_SERIALIZE_ENUM macros read from +template +struct is_basic_json_or_view : disjunction, is_json_view> {}; + +// the basic_json type of a basic_json value or of a view +template +struct json_type_of +{ + using type = T; +}; + +template +struct json_type_of::value>> +{ + using type = typename T::basic_json_type; +}; + +template +using json_type_of_t = typename json_type_of::type; + // Reimplementation of is_constructible and is_default_constructible, due to them being broken for // std::pair and std::tuple until LWG 2367 fix (see https://cplusplus.github.io/LWG/lwg-defects.html#2367). // This causes compile errors in e.g., Clang 3.5 or GCC 4.9. @@ -6035,6 +6083,59 @@ NLOHMANN_JSON_NAMESPACE_END // #include +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +// #include + + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ + +/// how to treat decoding errors +/// +/// @ref basic_json::dump uses this to decide what to do with ill-formed +/// UTF-8 while escaping a string, and the binary writers (@ref +/// basic_json::to_cbor, @ref basic_json::to_ubjson, @ref +/// basic_json::to_bjdata, @ref basic_json::to_bson) use it the same way for +/// string values and object keys. The binary readers (@ref +/// basic_json::from_cbor, @ref basic_json::from_msgpack, @ref +/// basic_json::from_ubjson, @ref basic_json::from_bjdata, @ref +/// basic_json::from_bson) use it to decide whether to check text strings +/// and object keys for well-formed UTF-8 at all, since none of those +/// formats requires a decoder to do so. +enum class error_handler_t +{ + strict, ///< throw a type_error/parse_error exception in case of invalid UTF-8 + replace, ///< replace invalid UTF-8 sequences with U+FFFD + ignore, ///< ignore invalid UTF-8 sequences + keep ///< keep invalid UTF-8 sequences unchanged +}; + +/// the default error handler of the CBOR, UBJSON, BJData, and BSON writers: +/// error_handler_t::strict if JSON_STRICT_BINARY_UTF8 is enabled, otherwise +/// error_handler_t::keep (the behavior before version 3.13.0) +constexpr error_handler_t binary_writer_default_error_handler() noexcept +{ +#if JSON_STRICT_BINARY_UTF8 + return error_handler_t::strict; +#else + return error_handler_t::keep; +#endif +} + +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + // #include // #include @@ -6675,16 +6776,114 @@ inline void from_json(const BasicJsonType& j, std_fs::path& p) } #endif -struct from_json_fn +namespace from_json_adl { - template - auto operator()(const BasicJsonType& j, T&& val) const +// hides the library's from_json() overloads above (and the from_json object): +// a call from this namespace finds only the from_json() functions of the +// arguments' own namespaces and classes (ADL); the declaration is never viable +// and never defined (not "= delete", which GCC before 6 reports as a use) +struct hidden; +void from_json(const hidden&); + +// a type no from_json() written for JSON values accepts: a from_json() that +// accepts it is not constrained to them (see has_view_from_json; it lives +// here, so that ADL finds no other from_json() for it) +struct probe +{}; + +struct fn +{ + template + auto operator()(const SourceType& j, T&& val) const noexcept(noexcept(from_json(j, std::forward(val)))) -> decltype(from_json(j, std::forward(val))) { return from_json(j, std::forward(val)); } }; +} // namespace from_json_adl + +// the sources whose from_json() is only looked up by ADL: views (the library's +// overloads are written for basic_json) and the probe +template +struct is_adl_only_from_json_source : disjunction, std::is_same> {}; + +struct from_json_fn +{ + template < typename BasicJsonType, typename T, + enable_if_t < !is_adl_only_from_json_source::value, int > = 0 > + auto operator()(const BasicJsonType& j, T && val) const + noexcept(noexcept(from_json(j, std::forward(val)))) + -> decltype(from_json(j, std::forward(val))) + { + return from_json(j, std::forward(val)); + } + + template < typename SourceType, typename T, + enable_if_t < is_adl_only_from_json_source::value, int > = 0 > + auto operator()(const SourceType& j, T && val) const + noexcept(noexcept(from_json_adl::fn {}(j, std::forward(val)))) + -> decltype(from_json_adl::fn {}(j, std::forward(val))) + { + return from_json_adl::fn {}(j, std::forward(val)); + } +}; + +// Whether JSONSerializer::from_json(view, T&) reads T from a view (of +// ) directly: it must accept the view but not the +// probe. An unconstrained from_json() template (written for basic_json, but +// accepting anything) is thus not called with a view, nor are the library's +// own conversions; the view converts such types through materialize(). +template +using serializer_from_json_t = decltype(Serializer::from_json(std::declval(), std::declval())); + +template +struct has_view_from_json + : conjunction < is_detected, + negation>> {}; + +// the base part of a type of the NLOHMANN_DEFINE_DERIVED_TYPE_* macros: from a +// view through its get_to(), which reads the base from the view where the +// base's from_json() accepts views, and from materialize() otherwise +template::value, int> = 0> +void from_json_base(const BasicJsonType& j, T& base) +{ + from_json_fn {}(j, base); +} + +template::value, int> = 0> +void from_json_base(const View& v, T& base) +{ + v.get_to(base); +} + +// the context of an exception thrown by a from_json() of the macros: the value +// itself for basic_json (for JSON_DIAGNOSTICS), none for a view +template::value, int> = 0> +const BasicJsonType * from_json_context(const BasicJsonType& j) noexcept +{ + return &j; +} + +template::value, int> = 0> +std::nullptr_t from_json_context(const View& /*unused*/) noexcept +{ + return nullptr; +} + +// a value as an exception message shows it (invalid UTF-8 replaced; the +// strings of a view are always valid UTF-8) +template::value, int> = 0> +typename BasicJsonType::string_t from_json_dump(const BasicJsonType& j) +{ + return j.dump(-1, ' ', false, error_handler_t::replace); +} + +template::value, int> = 0> +typename View::string_t from_json_dump(const View& v) +{ + return v.dump(); +} } // namespace detail @@ -6779,57 +6978,6 @@ NLOHMANN_JSON_NAMESPACE_END // #include // #include -// __ _____ _____ _____ -// __| | __| | | | JSON for Modern C++ -// | | |__ | | | | | | version 3.12.0 -// |_____|_____|_____|_|___| https://github.com/nlohmann/json -// -// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann -// SPDX-License-Identifier: MIT - - - -// #include - - -NLOHMANN_JSON_NAMESPACE_BEGIN -namespace detail -{ - -/// how to treat decoding errors -/// -/// @ref basic_json::dump uses this to decide what to do with ill-formed -/// UTF-8 while escaping a string, and the binary writers (@ref -/// basic_json::to_cbor, @ref basic_json::to_ubjson, @ref -/// basic_json::to_bjdata, @ref basic_json::to_bson) use it the same way for -/// string values and object keys. The binary readers (@ref -/// basic_json::from_cbor, @ref basic_json::from_msgpack, @ref -/// basic_json::from_ubjson, @ref basic_json::from_bjdata, @ref -/// basic_json::from_bson) use it to decide whether to check text strings -/// and object keys for well-formed UTF-8 at all, since none of those -/// formats requires a decoder to do so. -enum class error_handler_t -{ - strict, ///< throw a type_error/parse_error exception in case of invalid UTF-8 - replace, ///< replace invalid UTF-8 sequences with U+FFFD - ignore, ///< ignore invalid UTF-8 sequences - keep ///< keep invalid UTF-8 sequences unchanged -}; - -/// the default error handler of the CBOR, UBJSON, BJData, and BSON writers: -/// error_handler_t::strict if JSON_STRICT_BINARY_UTF8 is enabled, otherwise -/// error_handler_t::keep (the behavior before version 3.13.0) -constexpr error_handler_t binary_writer_default_error_handler() noexcept -{ -#if JSON_STRICT_BINARY_UTF8 - return error_handler_t::strict; -#else - return error_handler_t::keep; -#endif -} - -} // namespace detail -NLOHMANN_JSON_NAMESPACE_END NLOHMANN_JSON_NAMESPACE_BEGIN diff --git a/single_include/nlohmann/json_view.hpp b/single_include/nlohmann/json_view.hpp index 0e6c087ab..44a7c62d9 100644 --- a/single_include/nlohmann/json_view.hpp +++ b/single_include/nlohmann/json_view.hpp @@ -7858,6 +7858,8 @@ class basic_json_view using navigation = detail::view::navigation; public: + /// the basic_json type of the document (it also marks views for is_json_view) + using basic_json_type = BasicJsonType; using value_t = detail::value_t; using string_t = typename BasicJsonType::string_t; using number_integer_t = typename BasicJsonType::number_integer_t; @@ -8328,8 +8330,10 @@ class basic_json_view /// the value converted to T, as BasicJsonType::get(): arithmetic types, /// strings (string_view_t without a copy), std::nullptr_t, std::vector, - /// maps with string keys, and views are converted directly; other types - /// through materialize().get() + /// maps with string keys, and views are converted directly; types whose + /// from_json() accepts views (those of the NLOHMANN_DEFINE_TYPE_* and + /// NLOHMANN_JSON_SERIALIZE_ENUM macros, say) read the view; other types + /// go through materialize().get() template NLOHMANN_VIEW_ALWAYS_INLINE T get() const { @@ -8337,11 +8341,12 @@ class basic_json_view } /// as BasicJsonType::get_to(): the types get() converts directly are - /// assigned, all others are passed to from_json with v as it is + /// assigned, all others are passed to from_json with v as it is (with + /// the view if from_json() accepts views, else with materialize()) template T& get_to(T& v) const { - get_to_impl(v, detail::view::is_direct_value {}); + get_to_impl(v, get_to_kind {}); return v; } @@ -8684,6 +8689,26 @@ class basic_json_view return detail::view::map_value>(*this); } + /// whether the from_json() of T's serializer reads views (see + /// detail::has_view_from_json) + template + using reads_view = detail::has_view_from_json, basic_json_view, T>; + + /// how get_to() converts: 2 directly, 1 with a from_json() that reads + /// views, 0 through materialize() + template + using get_to_kind = std::integral_constant < int, detail::view::is_direct_value::value + ? 2 : static_cast(reads_view::value) >; + + /// types whose from_json() reads views, without materialize() + template < typename T, typename std::enable_if < reads_view::value && std::is_default_constructible::value, int >::type = 0 > + T get_impl(detail::view::value_tag /*unused*/, detail::priority_tag<1> /*unused*/) const + { + auto v = T(); + BasicJsonType::template json_serializer::from_json(*this, v); + return v; + } + /// everything else through the BasicJsonType value (from_json included) template T get_impl(detail::view::value_tag /*unused*/, detail::priority_tag<0> /*unused*/) const @@ -8694,14 +8719,20 @@ class basic_json_view // --- get_to() dispatch --- template - void get_to_impl(T& v, std::true_type /*direct*/) const + void get_to_impl(T& v, std::integral_constant /*direct*/) const { v = get(); } /// from_json may keep parts of v (members a JSON object lacks, say) template - void get_to_impl(T& v, std::false_type /*direct*/) const + void get_to_impl(T& v, std::integral_constant /*from_json reads views*/) const + { + BasicJsonType::template json_serializer::from_json(*this, v); + } + + template + void get_to_impl(T& v, std::integral_constant /*through materialize()*/) const { materialize().get_to(v); } @@ -9406,6 +9437,15 @@ class basic_json_document std::unique_ptr m_data{}; // NOLINT(readability-redundant-member-init) }; +/*! +@brief whether T is a basic_json_view (of any BasicJsonType and view_mode) + +For constraining templates that accept views, e.g. a from_json() written for +both basic_json values and views. +*/ +template +struct is_json_view : detail::is_json_view {}; + /// a parsed JSON text for json using json_document = basic_json_document; /// a value of a json_document diff --git a/tests/benchmarks/json_view/README.md b/tests/benchmarks/json_view/README.md index c4a05d35a..a0bd2afbc 100644 --- a/tests/benchmarks/json_view/README.md +++ b/tests/benchmarks/json_view/README.md @@ -59,6 +59,12 @@ An editable `json_document` edits in place; yyjson copies its immutable document (`yyjson_doc_mut_copy`); Boost.JSON and `json::parse` build mutable DOMs; simdjson cannot edit a document. All outputs are checked to describe the same value. +`bench_decode.cpp` measures decoding into structs whose `from_json()` comes from the `NLOHMANN_DEFINE_TYPE_*` macros +(twitter statuses, citm_catalog performances, and 20,000 synthetic records): `get()` on a view, which reads the +view directly, against `materialize().get()`, `json::parse` plus `get()`, and `get()` on an already parsed +`json` value. Only nlohmann/json takes part, as the other libraries have no comparable mapping; all engines are +checked to decode the same values. + Before anything is timed, all engines must accept each document and agree on the traversal: the number of values, the bytes of all strings and keys, and the sum of all numbers. All engines run interleaved in every round, and the best round is reported, as time and as a factor of the `json_view` time (below 1 means faster than `json_view`). Each timed diff --git a/tests/benchmarks/json_view/bench_decode.cpp b/tests/benchmarks/json_view/bench_decode.cpp new file mode 100644 index 000000000..29e3d7f81 --- /dev/null +++ b/tests/benchmarks/json_view/bench_decode.cpp @@ -0,0 +1,312 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +// Decoding into structs: the same types, with from_json() generated by the +// NLOHMANN_DEFINE_TYPE_* macros, read three ways. Only nlohmann/json takes +// part; the other libraries have no comparable mapping. +// +// view json_document::parse, then get() on the view (from_json reads the view) +// materialize json_document::parse, then materialize().get() (a json value first) +// json::parse json::parse, then get() +// json::get get() on an already parsed json value (the decoding alone) +// +// Documents: twitter (the statuses with their users and hashtags), citm_catalog +// (the performances with their seat categories), and a synthetic document of +// 20,000 nested records. +// +// Build: see README.md (same flags as bench_view.cpp). +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using nlohmann::json; +using nlohmann::json_document; + +static volatile std::size_t g_sink; + +namespace twitter +{ +struct hashtag +{ + std::string text; + std::vector indices; +}; +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(hashtag, text, indices) + +struct entities +{ + std::vector hashtags; +}; +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(entities, hashtags) + +struct user +{ + std::uint64_t id = 0; + std::string screen_name; + std::string description; + std::int64_t followers_count = 0; +}; +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(user, id, screen_name, description, followers_count) + +struct status +{ + std::uint64_t id = 0; + std::string text; + user user; + entities entities; + std::int64_t retweet_count = 0; + std::int64_t favorite_count = 0; +}; +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(status, id, text, user, entities, retweet_count, favorite_count) + +struct document +{ + std::vector statuses; +}; +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(document, statuses) + +std::size_t digest(const document& d) +{ + std::size_t n = 0; + for (const auto& s : d.statuses) + { + n += s.text.size() + s.user.screen_name.size() + s.entities.hashtags.size() + static_cast(s.retweet_count); + } + return n; +} +} // namespace twitter + +namespace citm +{ +struct area +{ + std::int64_t areaId = 0; + std::vector blockIds; +}; +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(area, areaId, blockIds) + +struct seat_category +{ + std::vector areas; + std::int64_t seatCategoryId = 0; +}; +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(seat_category, areas, seatCategoryId) + +struct performance +{ + std::int64_t id = 0; + std::int64_t eventId = 0; + std::vector seatCategories; + std::int64_t start = 0; + std::string venueCode; +}; +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(performance, id, eventId, seatCategories, start, venueCode) + +struct document +{ + std::vector performances; +}; +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(document, performances) + +std::size_t digest(const document& d) +{ + std::size_t n = 0; + for (const auto& p : d.performances) + { + n += p.seatCategories.size() + p.venueCode.size() + static_cast(p.start % 1000); + for (const auto& c : p.seatCategories) + { + n += c.areas.size(); + } + } + return n; +} +} // namespace citm + +namespace synthetic +{ +struct position +{ + double x = 0; + double y = 0; +}; +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(position, x, y) + +struct child +{ + std::int64_t id = 0; + std::string label; + bool active = false; +}; +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(child, id, label, active) + +struct record +{ + std::int64_t id = 0; + std::string name; + std::vector tags; + position pos; + std::vector children; +}; +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(record, id, name, tags, pos, children) + +struct document +{ + std::vector records; +}; +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(document, records) + +std::string make_text(std::size_t n) +{ + std::string s = "{\"records\":["; + for (std::size_t i = 0; i < n; ++i) + { + const std::string id = std::to_string(i); + s += (i == 0 ? "" : ","); + s += "{\"id\":" + id + ",\"name\":\"record number " + id + "\",\"tags\":[\"alpha\",\"beta\",\"gamma\"],"; + s += "\"pos\":{\"x\":" + id + ".25,\"y\":-" + id + ".5},\"children\":["; + for (std::size_t k = 0; k < 3; ++k) + { + s += (k == 0 ? "" : ","); + s += "{\"id\":" + std::to_string(i * 3 + k) + ",\"label\":\"child " + std::to_string(k) + "\",\"active\":" + (k % 2 == 0 ? "true" : "false") + "}"; + } + s += "]}"; + } + s += "]}"; + return s; +} + +std::size_t digest(const document& d) +{ + std::size_t n = 0; + for (const auto& r : d.records) + { + n += r.name.size() + r.tags.size() + r.children.size() + static_cast(r.pos.x); + } + return n; +} +} // namespace synthetic + +namespace +{ +std::string read_file(const std::string& p) +{ + std::ifstream f(p, std::ios::binary); + if (!f) + { + std::fprintf(stderr, "cannot open %s\n", p.c_str()); + std::exit(1); + } + std::stringstream ss; + ss << f.rdbuf(); + return ss.str(); +} + +double time_ns(const std::function& f) +{ + const auto t0 = std::chrono::steady_clock::now(); + f(); + const auto t1 = std::chrono::steady_clock::now(); + return static_cast(std::chrono::duration_cast(t1 - t0).count()); +} + +struct engine +{ + std::string name; + std::function run; +}; + +template +std::vector engines(const std::string& text, const json& parsed) +{ + return + { + {"view", [&text] { const json_document d = json_document::parse(text); return digest(d.root().get()); }}, + {"materialize", [&text] { const json_document d = json_document::parse(text); return digest(d.root().materialize().get()); }}, + {"json::parse", [&text] { return digest(json::parse(text).get()); }}, + {"json::get", [&parsed] { return digest(parsed.get()); }}, + }; +} +} // namespace + +int main(int argc, char** argv) +{ + if (argc < 2) + { + std::fprintf(stderr, "usage: %s [rounds]\n", argv[0]); + return 1; + } + const std::string dir = argv[1]; + const int rounds = argc > 2 ? std::max(1, std::atoi(argv[2])) : 30; + + struct document_case + { + std::string name; + std::string text; + json parsed; + std::vector engines; + }; + std::vector cases; + cases.push_back({"twitter", read_file(dir + "/nativejson-benchmark/twitter.json"), json(), {}}); + cases.push_back({"citm_catalog", read_file(dir + "/nativejson-benchmark/citm_catalog.json"), json(), {}}); + cases.push_back({"synthetic", synthetic::make_text(20000), json(), {}}); + for (auto& c : cases) + { + c.parsed = json::parse(c.text); + } + cases[0].engines = engines(cases[0].text, cases[0].parsed); + cases[1].engines = engines(cases[1].text, cases[1].parsed); + cases[2].engines = engines(cases[2].text, cases[2].parsed); + + std::FILE* csv = std::fopen("bench_decode.csv", "w"); + if (csv == nullptr) + { + std::perror("bench_decode.csv"); + return 1; + } + std::fprintf(csv, "doc,bytes,engine,ns\n"); + for (const auto& c : cases) + { + // all engines must decode the same values + const std::size_t expected = c.engines[0].run(); + bool ok = true; + for (const auto& e : c.engines) + { + ok = ok && e.run() == expected; + } + std::vector best(c.engines.size(), 1e300); + for (int r = 0; r < rounds; ++r) + { + for (std::size_t k = 0; k < c.engines.size(); ++k) + { + g_sink = c.engines[k].run(); // untimed: see bench_view.cpp + best[k] = std::min(best[k], time_ns([&] { g_sink = c.engines[k].run(); })); + } + } + std::printf("%-13s %-7s %s", c.name.c_str(), "decode", ok ? "" : "[MISMATCH] "); + for (std::size_t k = 0; k < c.engines.size(); ++k) + { + const double us = best[k] / 1000.0; + std::printf(" %s %.*fus (%.2fx)", c.engines[k].name.c_str(), us < 10 ? 3 : (us < 1000 ? 1 : 0), us, best[k] / best[0]); + std::fprintf(csv, "%s,%zu,%s,%.1f\n", c.name.c_str(), c.text.size(), c.engines[k].name.c_str(), best[k]); + } + std::printf("\n"); + } + std::fclose(csv); + return 0; +} diff --git a/tests/benchmarks/json_view/compare.py b/tests/benchmarks/json_view/compare.py index 39d5048f0..6573c88af 100755 --- a/tests/benchmarks/json_view/compare.py +++ b/tests/benchmarks/json_view/compare.py @@ -9,7 +9,7 @@ """Compare json_view with yyjson, simdjson, Boost.JSON, and json::parse. -Builds bench_view.cpp, bench_corpus.cpp, and bench_edit.cpp against the include/ directory of +Builds bench_view.cpp, bench_corpus.cpp, bench_edit.cpp, and bench_decode.cpp against the include/ directory of this checkout, runs them, and writes the results with everything needed to reproduce them (date, commit, CPU, OS, compiler, library versions, flags) to results/-.md and .csv next to this script. @@ -283,7 +283,7 @@ def main(): objects.append(obj) binaries = {} - for bench in ['bench_view', 'bench_corpus', 'bench_edit']: + for bench in ['bench_view', 'bench_corpus', 'bench_edit', 'bench_decode']: exe = os.path.join(args.build_dir, bench) run(cxx + flags + include + [os.path.join(HERE, bench + '.cpp')] + objects + link + ['-o', exe]) binaries[bench] = exe @@ -297,6 +297,8 @@ def main(): capture_output=True, text=True).stdout outputs['bench_edit'] = run([binaries['bench_edit'], args.data, str(max(1, args.rounds // 2))], cwd=args.build_dir, capture_output=True, text=True).stdout + outputs['bench_decode'] = run([binaries['bench_decode'], args.data, str(max(1, args.rounds // 2))], cwd=args.build_dir, + capture_output=True, text=True).stdout for name, text in outputs.items(): print(text) @@ -330,7 +332,7 @@ def main(): f.write(f'\n## {name}\n\n```\n{text.rstrip()}\n```\n') with open(stem + '.csv', 'w', encoding='utf-8') as out: out.write(''.join(f'# {key}: {value}\n' for key, value in meta)) - for name in ['bench_view', 'bench_corpus', 'bench_edit']: + for name in ['bench_view', 'bench_corpus', 'bench_edit', 'bench_decode']: path = os.path.join(args.build_dir, name + '.csv') if os.path.isfile(path): with open(path, encoding='utf-8') as f: diff --git a/tests/src/unit-json_view_from_json.cpp b/tests/src/unit-json_view_from_json.cpp new file mode 100644 index 000000000..f106b1fe0 --- /dev/null +++ b/tests/src/unit-json_view_from_json.cpp @@ -0,0 +1,357 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#include "doctest_compatibility.h" + +#include +using nlohmann::json; +using nlohmann::json_document; +using nlohmann::json_view; +using nlohmann::json_editable_document; +using nlohmann::ordered_json_document; + +#include "json_view_test_helpers.hpp" +#if !defined(JSON_NOEXCEPTION) + using json_view_test::exception_of_call; +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef JSON_HAS_CPP_17 + #include +#endif + +// The from_json() functions of the NLOHMANN_DEFINE_TYPE_* and +// NLOHMANN_JSON_SERIALIZE_ENUM macros read views directly; other from_json() +// functions get the value of materialize(). + +namespace view_from_json +{ +struct point +{ + int x = 0; + int y = 0; + NLOHMANN_DEFINE_TYPE_INTRUSIVE(point, x, y) +}; + +struct shape +{ + std::string name; + std::vector points; + point center; + std::map named; + NLOHMANN_DEFINE_TYPE_INTRUSIVE(shape, name, points, center, named) +}; + +struct with_defaults +{ + int a = 1; + std::string b = "default"; + std::vector c = {1, 2}; +}; +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(with_defaults, a, b, c) + +struct with_names +{ + int value = 0; + std::string text; +}; +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_NAMES(with_names, "Value", value, "Text", text) + +struct empty +{ + NLOHMANN_DEFINE_TYPE_INTRUSIVE(empty) +}; + +enum class color +{ + red, + green, + blue +}; +NLOHMANN_JSON_SERIALIZE_ENUM(color, +{ + {color::red, "red"}, + {color::green, "green"}, + {color::blue, nullptr}, +}) + +enum class strict_color +{ + red, + green +}; +NLOHMANN_JSON_SERIALIZE_ENUM_STRICT(strict_color, +{ + {strict_color::red, "red"}, + {strict_color::green, 1}, +}) + +struct base +{ + int id = 0; + NLOHMANN_DEFINE_TYPE_INTRUSIVE(base, id) +}; + +struct derived : base +{ + color c = color::red; + NLOHMANN_DEFINE_DERIVED_TYPE_INTRUSIVE(derived, base, c) +}; + +struct derived_with_defaults : base +{ + std::string label = "none"; + NLOHMANN_DEFINE_DERIVED_TYPE_INTRUSIVE_WITH_DEFAULT(derived_with_defaults, base, label) +}; + +// a base whose from_json() is written for json only: the derived type reads +// its own members from the view and the base part from materialize() +struct legacy_base +{ + int id = 0; +}; +// NOLINTNEXTLINE(misc-use-internal-linkage): found by ADL, like the from_json() functions of users +inline void from_json(const json& j, legacy_base& b) +{ + b.id = j.at("id").get(); +} + +struct derived_from_legacy : legacy_base +{ + int extra = 0; +}; +NLOHMANN_DEFINE_DERIVED_TYPE_NON_INTRUSIVE(derived_from_legacy, legacy_base, extra) + +// from_json() for json only: converted through materialize() +struct legacy +{ + int v = 0; +}; +// NOLINTNEXTLINE(misc-use-internal-linkage): found by ADL, like the from_json() functions of users +inline void from_json(const json& j, legacy& l) +{ + l.v = j.at("v").get(); +} + +// an unconstrained from_json() template, as in the documentation of +// arbitrary type conversions: converted through materialize() +struct unconstrained +{ + int v = 0; +}; +template +// NOLINTNEXTLINE(misc-use-internal-linkage): found by ADL, like the from_json() functions of users +void from_json(const BasicJsonType& j, unconstrained& u) +{ + u.v = j.at("v").template get(); +} + +// a from_json() constrained by the user to views and json: reads the view +struct constrained +{ + int v = 0; +}; +template < typename J, typename std::enable_if < nlohmann::is_json_view::value || std::is_same::value, int >::type = 0 > +// NOLINTNEXTLINE(misc-use-internal-linkage): found by ADL, like the from_json() functions of users +void from_json(const J& j, constrained& c) +{ + c.v = j.at("v").template get(); +} +} // namespace view_from_json + +namespace +{ +template +struct reads_view : nlohmann::detail::has_view_from_json, View, T> {}; +} // namespace + +TEST_CASE("json_view from_json") +{ + using namespace view_from_json; // NOLINT(google-build-using-namespace) + + SECTION("which from_json() functions read views") + { + static_assert(reads_view::value, "macro types read views"); + static_assert(reads_view::value, "macro types read views"); + static_assert(reads_view::value, "macro types read views"); + static_assert(reads_view::value, "macro types read views"); + static_assert(reads_view::value, "macro types read views"); + static_assert(reads_view::value, "macro types read views"); + static_assert(reads_view::value, "macro types read views"); + static_assert(reads_view::value, "enum macro types read views"); + static_assert(reads_view::value, "enum macro types read views"); + static_assert(reads_view::value, "constrained from_json() reads views"); + static_assert(reads_view::value, "editable views as well"); + static_assert(reads_view::value, "views of ordered_json as well"); + static_assert(!reads_view::value, "from_json() for json"); + static_assert(!reads_view::value, "from_json() for json"); + static_assert(!reads_view::value, "unconstrained from_json()"); + + static_assert(nlohmann::is_json_view::value, "is_json_view"); + static_assert(nlohmann::is_json_view::value, "is_json_view"); + static_assert(!nlohmann::is_json_view::value, "is_json_view"); + static_assert(!nlohmann::is_json_view::value, "is_json_view"); + static_assert(!nlohmann::is_json_view::value, "is_json_view"); + } + + SECTION("the library's own conversions are not called with a view (and the probe compiles for them)") + { + // these go through materialize() (vectors, maps with string keys, + // strings, and numbers are converted by the view itself) + static_assert(!reads_view>::value, "std types"); + static_assert(!reads_view>::value, "std types"); + static_assert(!reads_view>::value, "std types"); + static_assert(!reads_view>::value, "std types"); + static_assert(!reads_view>::value, "std types"); + static_assert(!reads_view>::value, "std types"); + static_assert(!reads_view>::value, "std types"); + static_assert(!reads_view>::value, "std types"); + static_assert(!reads_view>::value, "std types"); + static_assert(!reads_view>::value, "std types"); + static_assert(!reads_view>::value, "std types"); + static_assert(!reads_view>::value, "std types"); + static_assert(!reads_view>::value, "std types"); + static_assert(!reads_view>::value, "std types"); + static_assert(!reads_view>::value, "std types"); + static_assert(!reads_view>::value, "std types"); + static_assert(!reads_view::value, "std types"); + static_assert(!reads_view::value, "std types"); + static_assert(!reads_view::value, "std types"); + static_assert(!reads_view::value, "std types"); + static_assert(!reads_view::value, "std types"); + static_assert(!reads_view::value, "std types"); + static_assert(!reads_view::value, "std types"); +#ifdef JSON_HAS_CPP_17 + static_assert(!reads_view>::value, "std types"); +#endif + CHECK(true); + } + + SECTION("macro types") + { + const json_document d = json_document::parse(R"({"name": "s", "points": [{"x": 1, "y": 2}, {"x": 3, "y": 4}], + "center": {"x": 5, "y": 6}, "named": {"a": {"x": 7, "y": 8}}})"); + const shape s = d.root().get(); + CHECK(s.name == "s"); + CHECK(s.points.size() == 2); + CHECK(s.points[1].y == 4); + CHECK(s.center.x == 5); + CHECK(s.named.at("a").y == 8); + + point p; + d.root()["center"].get_to(p); + CHECK(p.x == 5); + CHECK(p.y == 6); + + static_cast(d.root()["center"].get()); + } + + SECTION("defaults and names") + { + const json_document d = json_document::parse(R"({"a": 5, "Value": 3, "Text": "t"})"); + const with_defaults w = d.root().get(); + CHECK(w.a == 5); + CHECK(w.b == "default"); + CHECK(w.c == std::vector {1, 2}); + const json_document null_document = json_document::parse("null"); + CHECK(null_document.root().get().a == 1); + const with_names n = d.root().get(); + CHECK(n.value == 3); + CHECK(n.text == "t"); + } + + SECTION("enums") + { + const json_document d = json_document::parse(R"(["green", null, "purple", 1, "red"])"); + CHECK(d.root()[0].get() == color::green); + CHECK(d.root()[1].get() == color::blue); + CHECK(d.root()[2].get() == color::red); // unknown values map to the first entry + CHECK(d.root()[3].get() == strict_color::green); + CHECK(d.root()[4].get() == strict_color::red); +#if !defined(JSON_NOEXCEPTION) + const json j = d.root().materialize(); + CHECK(exception_of_call([&] { static_cast(d.root()[2].get()); }) == "[json.exception.out_of_range.410] enum value out of range for strict_color: \"purple\""); +#if !JSON_DIAGNOSTICS + CHECK(exception_of_call([&] { static_cast(d.root()[2].get()); }) == exception_of_call([&] { static_cast(j[2].get()); })); +#endif +#endif + } + + SECTION("derived types") + { + const json_document d = json_document::parse(R"({"id": 7, "c": "green", "extra": 3})"); + const derived x = d.root().get(); + CHECK(x.id == 7); + CHECK(x.c == color::green); + const derived_with_defaults y = d.root().get(); + CHECK(y.id == 7); + CHECK(y.label == "none"); + const derived_from_legacy z = d.root().get(); + CHECK(z.id == 7); + CHECK(z.extra == 3); + } + + SECTION("other from_json() functions get the value of materialize()") + { + const json_document d = json_document::parse(R"({"v": 4})"); + CHECK(d.root().get().v == 4); + CHECK(d.root().get().v == 4); + CHECK(d.root().get().v == 4); + legacy l; + d.root().get_to(l); + CHECK(l.v == 4); + } + + SECTION("duplicate keys: members are looked up in the view, where the first one wins") + { + const json_document d = json_document::parse(R"({"x": 1, "y": 2, "x": 3})"); + CHECK(d.root().get().x == 1); + // materialize() keeps the last value, like parse() + CHECK(d.root().materialize().get().x == 3); + } + + SECTION("editable documents and ordered_json") + { + json_editable_document e = json_editable_document::parse(R"({"x": 1, "y": 2})"); + e.set(e.root(), "x", 10); + CHECK(e.root().get().x == 10); + const ordered_json_document o = ordered_json_document::parse(R"({"name": "o", "points": [], "center": {"x": 1, "y": 2}, "named": {}})"); + CHECK(o.root().get().center.y == 2); + } + +#if !defined(JSON_NOEXCEPTION) && !JSON_DIAGNOSTICS + SECTION("errors are those of the same conversion of a json value") + { + // (with JSON_DIAGNOSTICS, those of json name the path of the value) + const json_document d = json_document::parse(R"([{"x": 1}, {"x": "1", "y": 2}, [1, 2], {"id": "7", "c": "red"}])"); + const json j = d.root().materialize(); + for (std::size_t i = 0; i < 3; ++i) + { + CAPTURE(i) + CHECK(exception_of_call([&] { static_cast(d.root()[i].get()); }) == exception_of_call([&] { static_cast(j[i].get()); })); + } + CHECK(exception_of_call([&] { static_cast(d.root()[3].get()); }) == exception_of_call([&] { static_cast(j[3].get()); })); + } +#endif +} From 9e67bfa11503eeb8b4a39ff83189bd92c5a91702 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Mon, 26 Oct 2026 20:13:31 +0100 Subject: [PATCH 17/18] json_view: the library's std conversions read views directly (#5923) * json_view: the library's std conversions read views directly The library's from_json() overloads (containers, maps, std::array, C arrays, pairs, tuples, optional, valarray, forward_list, enums, binary, paths, and scalars) accept views: their traits use the view's basic_json type, and the few accesses that differ (get_ptr) go through small helpers. from_json() of a view now finds them; only the probe still looks up by ADL only. get() of a view thus converts these types without materialize(), with the same values and exceptions; from an object, a repeated key keeps the last value, as parse() does. The view keeps its direct paths for vectors of the types it converts directly (faster than from_json()) and for containers of views; its other vector and map paths are replaced by from_json(). For basic_json, std::valarray and std::forward_list are read in one pass (40 % and 3.5 % fewer instructions); all other conversions are unchanged. Signed-off-by: Niels Lohmann * json_view: enable_if_not_view_t instead of '!' in the from_json() templates astyle spaces out the template argument list when it contains a negation; the alias spells the condition with negation<> and leaves the lines alone. Signed-off-by: Niels Lohmann * json_view: compact the template parameter lists astyle had spaced out Signed-off-by: Niels Lohmann --------- Signed-off-by: Niels Lohmann --- docs/mkdocs/docs/api/basic_json_view/get.md | 33 +- .../mkdocs/docs/api/basic_json_view/get_to.md | 3 +- .../nlohmann/detail/conversions/from_json.hpp | 284 +++++++++++----- include/nlohmann/detail/exceptions.hpp | 16 +- include/nlohmann/detail/macro_scope.hpp | 2 +- include/nlohmann/detail/view/value.hpp | 12 +- include/nlohmann/json_view.hpp | 19 +- single_include/nlohmann/json.hpp | 302 +++++++++++++----- single_include/nlohmann/json_view.hpp | 31 +- tests/src/unit-json_view_from_json.cpp | 146 +++++++-- 10 files changed, 618 insertions(+), 230 deletions(-) diff --git a/docs/mkdocs/docs/api/basic_json_view/get.md b/docs/mkdocs/docs/api/basic_json_view/get.md index 40fd1033a..cd0a98bff 100644 --- a/docs/mkdocs/docs/api/basic_json_view/get.md +++ b/docs/mkdocs/docs/api/basic_json_view/get.md @@ -19,16 +19,19 @@ For the types below, the conversion works directly on the flat index -- no `Basi document's own buffer of decoded strings (see [`get_string()`](get_string.md)) - `BasicJsonType` -- equivalent to [`materialize()`](materialize.md) - `basic_json_view` -- returns `#!cpp *this` -- `#!cpp std::vector` -- from an array, element by element, each converted with `#!cpp get()`; `#!cpp - std::vector` keeps a view of every element instead of a value -- `#!cpp std::map` and `#!cpp std::unordered_map`, if `K` is constructible from a `#!cpp - (const char*, std::size_t)` pair -- from an object, member by member, each value converted with `#!cpp get()`; with a repeated - key, the *last* value is kept, as [`BasicJsonType::parse()`](../basic_json/parse.md) (and - [`materialize()`](materialize.md)) does; `#!cpp std::map` keeps views of the members - instead of values +- `#!cpp std::vector` of the types above -- from an array, element by element, each converted with + `#!cpp get()`; `#!cpp std::vector` keeps a view of every element instead of a value +- `#!cpp std::map` and `#!cpp std::unordered_map`, if `K` is + constructible from a `#!cpp (const char*, std::size_t)` pair -- views of the members of an object; with a repeated + key, the view of the *last* member is kept, as [`BasicJsonType::parse()`](../basic_json/parse.md) keeps its value -A default-constructible `T` whose `from_json()` accepts views reads the view directly as well, member by member, without -building a `BasicJsonType` value. This covers the types of the +A default-constructible `T` whose `from_json()` accepts views reads the view directly as well, without building a +`BasicJsonType` value. This covers the library's own conversions of the standard library types -- all other +`#!cpp std::vector`s, `#!cpp std::map`/`#!cpp std::unordered_map` (also with other key types), `#!cpp std::list`, +`#!cpp std::deque`, `#!cpp std::set`, `#!cpp std::forward_list`, `#!cpp std::valarray`, `#!cpp std::array`, C arrays, +`#!cpp std::pair`, `#!cpp std::tuple`, `#!cpp std::optional`, enumerations, `#!cpp std::filesystem::path`, ... -- +which give the same values as `#!cpp materialize().get()` (from an object, a repeated key keeps the *last* value, +as [`BasicJsonType::parse()`](../basic_json/parse.md) does), as well as the types of the [`NLOHMANN_DEFINE_TYPE_*`](../macros/nlohmann_define_type_intrusive.md) and [`NLOHMANN_DEFINE_DERIVED_TYPE_*`](../macros/nlohmann_define_derived_type.md) macros and the enumerations of [`NLOHMANN_JSON_SERIALIZE_ENUM`](../macros/nlohmann_json_serialize_enum.md), and any `from_json()` constrained to @@ -44,8 +47,8 @@ T&)` of [arbitrary type conversions](../../features/arbitrary_types.md), is not keeps it: for `#!cpp {"x":1,"x":2}`, `#!cpp get()` reads `#!cpp x == 1`. See [Duplicate keys](../../features/json_view.md#duplicate-keys). -Every other `T` -- `#!cpp std::list`, `#!cpp std::pair`, `#!cpp std::array`, user types whose `from_json()` is -written for `BasicJsonType` only, ... -- is converted by `#!cpp materialize().get()`: the subtree is built into a real `BasicJsonType` +Every other `T` -- user types whose `from_json()` is written for `BasicJsonType` only, types without a default +constructor, `#!cpp std::tuple`s holding references, ... -- is converted by `#!cpp materialize().get()`: the subtree is built into a real `BasicJsonType` value first (as [`BasicJsonType::parse()`](../basic_json/parse.md) would), and converted from there exactly as [`BasicJsonType::get()`](../basic_json/get.md) would convert it. @@ -70,10 +73,10 @@ Strong guarantee: there are no changes to the view or the document it refers to. throws for the same JSON type and `T`. - For `#!cpp std::vector`: throws `type_error.302` if the value is not an array; otherwise, whatever converting an element to `U` throws. -- For `#!cpp std::map`/`#!cpp std::unordered_map`: throws `type_error.302` if the value is not an object; otherwise, - whatever converting a member to the mapped type throws. -- For a `T` whose `from_json()` reads the view: whatever that `from_json()` throws; for the macros, the same - exceptions with the same messages as [`BasicJsonType::get()`](../basic_json/get.md) of the same value. +- For `#!cpp std::map`/`#!cpp std::unordered_map` of views: throws `type_error.302` if the value is not an object. +- For a `T` whose `from_json()` reads the view: whatever that `from_json()` throws; for the library's conversions and + the macros, the same exceptions with the same messages as [`BasicJsonType::get()`](../basic_json/get.md) of the + same value. - For every other `T`: whatever [`materialize().get()`](../basic_json/get.md) throws -- typically `type_error.302`, or whatever a user-provided `from_json()` throws. diff --git a/docs/mkdocs/docs/api/basic_json_view/get_to.md b/docs/mkdocs/docs/api/basic_json_view/get_to.md index 4d2dd2fa5..d11e7fd1a 100644 --- a/docs/mkdocs/docs/api/basic_json_view/get_to.md +++ b/docs/mkdocs/docs/api/basic_json_view/get_to.md @@ -9,8 +9,7 @@ Converts the value to `T` and stores it in `v`, in one of three ways: - For the types that [`get()`](get.md) converts directly -- arithmetic types, `#!cpp bool`, `#!cpp std::nullptr_t`, [`string_view_t`](index.md#member-types), `#!cpp std::basic_string`, `BasicJsonType`, - `basic_json_view`, `#!cpp std::vector`, and `#!cpp std::map`/`#!cpp std::unordered_map` with string keys -- `v` is - assigned: `#!cpp v = get()`. + `basic_json_view`, vectors of these, and maps of views -- `v` is assigned: `#!cpp v = get()`. - For a type whose `from_json()` accepts views (see [`get()`](get.md)), that `from_json()` is called with the view and the existing `v`, without building a `BasicJsonType` value. - For every other type, the subtree is built into a `BasicJsonType` value and `#!cpp materialize().get_to(v)` is diff --git a/include/nlohmann/detail/conversions/from_json.hpp b/include/nlohmann/detail/conversions/from_json.hpp index 696afc34b..8fb9d149b 100644 --- a/include/nlohmann/detail/conversions/from_json.hpp +++ b/include/nlohmann/detail/conversions/from_json.hpp @@ -45,6 +45,70 @@ NLOHMANN_JSON_NAMESPACE_BEGIN namespace detail { +// The from_json() overloads below read basic_json values and the views of +// (BasicJsonType is then a view, and +// json_type_of_t its basic_json type). The helpers below are +// the accesses that differ between the two. + +// the enable_if parameter of the overloads that read basic_json values (written +// with negation<> because astyle spaces out a template argument list with '!') +template +using enable_if_not_view_t = enable_if_t>::value, int>; + +// the boolean of j, a boolean +template = 0> +typename BasicJsonType::boolean_t boolean_value(const BasicJsonType& j) +{ + return *j.template get_ptr(); +} + +template::value, int> = 0> +bool boolean_value(const View& v) +{ + return v.template get(); +} + +// the string of j, a string +template = 0> +const typename BasicJsonType::string_t& string_value(const BasicJsonType& j) +{ + return *j.template get_ptr(); +} + +template::value, int> = 0> +typename View::string_t string_value(const View& v) +{ + const auto s = v.get_string(); + return typename View::string_t(s.data(), s.size()); +} + +// the binary value of j, a binary value (never a view: a view of a JSON text +// holds no binary values) +template = 0> +const typename BasicJsonType::binary_t& binary_value(const BasicJsonType& j) +{ + return *j.template get_ptr(); +} + +template::value, int> = 0> +typename View::basic_json_type::binary_t binary_value(const View& /*unused*/) +{ + return {}; // LCOV_EXCL_LINE +} + +// the elements of j, an array, for a range-based for loop +template = 0> +const typename BasicJsonType::array_t& array_elements(const BasicJsonType& j) +{ + return *j.template get_ptr(); +} + +template::value, int> = 0> +View array_elements(const View& v) noexcept +{ + return v; // (a view is two pointers) +} + template inline void from_json(const BasicJsonType& j, typename std::nullptr_t& n) { @@ -73,7 +137,7 @@ void from_json(const BasicJsonType& j, std::optional& opt) // read a number value of any of the three number types into val; // returns false (leaving val untouched) if j is not a number -template +template = 0> bool get_number_value(const BasicJsonType& j, ArithmeticType& val) { switch (static_cast(j)) @@ -110,10 +174,22 @@ bool get_number_value(const BasicJsonType& j, ArithmeticType& val) } } +// a view converts numbers itself (as basic_json does) +template::value, int> = 0> +bool get_number_value(const View& v, ArithmeticType& val) +{ + if (!v.is_number()) + { + return false; + } + val = v.template get(); + return true; +} + // overloads for basic_json template parameters template < typename BasicJsonType, typename ArithmeticType, enable_if_t < std::is_arithmetic::value&& - !std::is_same::value, + !std::is_same::boolean_t>::value, int > = 0 > void get_arithmetic_value(const BasicJsonType& j, ArithmeticType& val) { @@ -124,31 +200,31 @@ void get_arithmetic_value(const BasicJsonType& j, ArithmeticType& val) } template -inline void from_json(const BasicJsonType& j, typename BasicJsonType::boolean_t& b) +inline void from_json(const BasicJsonType& j, typename json_type_of_t::boolean_t& b) { if (JSON_HEDLEY_UNLIKELY(!j.is_boolean())) { throw_type_must_be("boolean", j); } - b = *j.template get_ptr(); + b = boolean_value(j); } template -inline void from_json(const BasicJsonType& j, typename BasicJsonType::string_t& s) +inline void from_json(const BasicJsonType& j, typename json_type_of_t::string_t& s) { if (JSON_HEDLEY_UNLIKELY(!j.is_string())) { throw_type_must_be("string", j); } - s = *j.template get_ptr(); + s = string_value(j); } template < typename BasicJsonType, typename StringType, enable_if_t < - std::is_assignable::value - && is_detected_exact::value - && !std::is_same::value + std::is_assignable::string_t>::value + && is_detected_exact::string_t::value_type, value_type_t, StringType>::value + && !std::is_same::string_t, StringType>::value && !is_json_ref::value, int > = 0 > inline void from_json(const BasicJsonType& j, StringType& s) { @@ -157,23 +233,23 @@ inline void from_json(const BasicJsonType& j, StringType& s) throw_type_must_be("string", j); } - s = *j.template get_ptr(); + s = string_value(j); } template -inline void from_json(const BasicJsonType& j, typename BasicJsonType::number_float_t& val) +inline void from_json(const BasicJsonType& j, typename json_type_of_t::number_float_t& val) { get_arithmetic_value(j, val); } template -inline void from_json(const BasicJsonType& j, number_unsigned_overload_t& val) +inline void from_json(const BasicJsonType& j, number_unsigned_overload_t>& val) { get_arithmetic_value(j, val); } template -inline void from_json(const BasicJsonType& j, typename BasicJsonType::number_integer_t& val) +inline void from_json(const BasicJsonType& j, typename json_type_of_t::number_integer_t& val) { get_arithmetic_value(j, val); } @@ -185,8 +261,8 @@ inline void from_json(const BasicJsonType& j, EnumType& e) { using underlying_type = typename std::underlying_type::type; // get_arithmetic_value() does not accept boolean_t; read the number that to_json() wrote instead - using value_type = typename std::conditional::value, - typename BasicJsonType::number_unsigned_t, underlying_type>::type; + using value_type = typename std::conditional::boolean_t>::value, + typename json_type_of_t::number_unsigned_t, underlying_type>::type; value_type val; get_arithmetic_value(j, val); e = static_cast(bool_aware_static_cast(val)); @@ -195,7 +271,7 @@ inline void from_json(const BasicJsonType& j, EnumType& e) // forward_list doesn't have an insert method template::value, int> = 0> + enable_if_t, T>::value, int> = 0> inline void from_json(const BasicJsonType& j, std::forward_list& l) { if (JSON_HEDLEY_UNLIKELY(!j.is_array())) @@ -203,16 +279,16 @@ inline void from_json(const BasicJsonType& j, std::forward_list& l throw_type_must_be("array", j); } l.clear(); - std::transform(j.rbegin(), j.rend(), - std::front_inserter(l), [](const BasicJsonType & i) + auto pos = l.before_begin(); + for (const auto& elem : array_elements(j)) { - return i.template get(); - }); + pos = l.insert_after(pos, elem.template get()); + } } // valarray doesn't have an insert method template::value, int> = 0> + enable_if_t, T>::value, int> = 0> inline void from_json(const BasicJsonType& j, std::valarray& l) { if (JSON_HEDLEY_UNLIKELY(!j.is_array())) @@ -220,11 +296,11 @@ inline void from_json(const BasicJsonType& j, std::valarray& l) throw_type_must_be("array", j); } l.resize(j.size()); - std::transform(j.begin(), j.end(), std::begin(l), - [](const BasicJsonType & elem) + std::size_t i = 0; + for (const auto& elem : array_elements(j)) { - return elem.template get(); - }); + l[i++] = elem.template get(); + } } // element is not itself a C array: read it directly @@ -252,7 +328,8 @@ auto from_json(const BasicJsonType& j, T (&arr)[N]) // NOLINT(cppcoreguidelines from_json_c_array_element(j, arr); } -template +// (for a view, array_t is read element by element below) +template = 0> inline void from_json_array_impl(const BasicJsonType& j, typename BasicJsonType::array_t& arr, priority_tag<3> /*unused*/) { arr = *j.template get_ptr(); @@ -261,7 +338,7 @@ inline void from_json_array_impl(const BasicJsonType& j, typename BasicJsonType: // fill a std::array from the JSON array j: the size is checked once and the // elements are read in order, so a short array converts the elements it has and // then throws the out_of_range error at() would throw for the first missing one -template +template = 0> void from_json_std_array(const BasicJsonType& j, std::array& arr) { const auto& inner_array = *j.template get_ptr(); @@ -276,6 +353,21 @@ void from_json_std_array(const BasicJsonType& j, std::array& arr) } } +// (a view has no random access to the elements: they are read in order) +template::value, int> = 0> +void from_json_std_array(const View& v, std::array& arr) +{ + std::size_t size = 0; + for (auto it = v.begin(); size < N && it != v.end(); ++it) + { + arr[size++] = (*it).template get(); + } + if (JSON_HEDLEY_UNLIKELY(size < N)) + { + static_cast(v.at(size)); + } +} + template auto from_json_array_impl(const BasicJsonType& j, std::array& arr, priority_tag<2> /*unused*/) @@ -324,9 +416,8 @@ auto from_json_array_impl(const BasicJsonType& j, ConstructibleArrayType& arr, p void()) { ConstructibleArrayType ret; - const auto* inner_array = j.template get_ptr(); - reserve_if_possible(ret, inner_array->size(), priority_tag<1> {}); - for (const auto& elem : *inner_array) + reserve_if_possible(ret, j.size(), priority_tag<1> {}); + for (const auto& elem : array_elements(j)) { // get() returns *this, this won't call a from_json // method when value_type is BasicJsonType @@ -337,11 +428,11 @@ auto from_json_array_impl(const BasicJsonType& j, ConstructibleArrayType& arr, p template < typename BasicJsonType, typename ConstructibleArrayType, enable_if_t < - is_constructible_array_type::value&& - !is_constructible_object_type::value&& - !is_constructible_string_type::value&& - !std::is_same::value&& - !is_compatible_binary_type::value&& + is_constructible_array_type, ConstructibleArrayType>::value&& + !is_constructible_object_type, ConstructibleArrayType>::value&& + !is_constructible_string_type, ConstructibleArrayType>::value&& + !std::is_same::binary_t>::value&& + !is_compatible_binary_type, ConstructibleArrayType>::value&& !is_basic_json::value, int > = 0 > auto from_json(const BasicJsonType& j, ConstructibleArrayType& arr) @@ -408,24 +499,24 @@ auto from_json(const BasicJsonType& j, identity_tag> tag) } template -inline void from_json(const BasicJsonType& j, typename BasicJsonType::binary_t& bin) +inline void from_json(const BasicJsonType& j, typename json_type_of_t::binary_t& bin) { if (JSON_HEDLEY_UNLIKELY(!j.is_binary())) { throw_type_must_be("binary", j); } - bin = *j.template get_ptr(); + bin = binary_value(j); } template < typename BasicJsonType, typename CompatibleArrayType, - enable_if_t < is_compatible_binary_type::value, + enable_if_t < is_compatible_binary_type, CompatibleArrayType>::value, int > = 0 > inline void from_json(const BasicJsonType& j, CompatibleArrayType& bin) { if (j.is_binary()) { - bin = static_cast(*j.template get_ptr()); + bin = static_cast(binary_value(j)); } else if (j.is_array()) { @@ -454,8 +545,47 @@ void from_json_object_emplace(ConstructibleObjectType& obj, Key&& key, Value&& v obj.emplace(std::forward(key), std::forward(value)); } +// a member of a view's object: a later member with the same key replaces an +// earlier one, as parse() keeps the last value of a repeated key (containers +// without operator[] keep the first) +template +auto from_json_object_assign(ConstructibleObjectType& obj, Key&& key, Value&& value, priority_tag<1> /*unused*/) +-> decltype(obj[std::forward(key)] = std::forward(value), void()) +{ + obj[std::forward(key)] = std::forward(value); +} + +template +void from_json_object_assign(ConstructibleObjectType& obj, Key&& key, Value&& value, priority_tag<0> /*unused*/) +{ + obj.emplace(std::forward(key), std::forward(value)); +} + +template = 0> +void from_json_object_impl(const BasicJsonType& j, ConstructibleObjectType& ret) +{ + const auto* inner_object = j.template get_ptr(); + reserve_if_possible(ret, inner_object->size(), priority_tag<1> {}); + for (const auto& p : *inner_object) + { + from_json_object_emplace(ret, p.first, p.second.template get(), priority_tag<1> {}); + } +} + +template::value, int> = 0> +void from_json_object_impl(const View& v, ConstructibleObjectType& ret) +{ + using string_t = typename View::basic_json_type::object_t::key_type; + reserve_if_possible(ret, v.size(), priority_tag<1> {}); + for (const auto item : v.items()) + { + const auto key = item.key(); + from_json_object_assign(ret, string_t(key.data(), key.size()), item.value().template get(), priority_tag<1> {}); + } +} + template::value, int> = 0> + enable_if_t, ConstructibleObjectType>::value, int> = 0> inline void from_json(const BasicJsonType& j, ConstructibleObjectType& obj) { if (JSON_HEDLEY_UNLIKELY(!j.is_object())) @@ -464,12 +594,7 @@ inline void from_json(const BasicJsonType& j, ConstructibleObjectType& obj) } ConstructibleObjectType ret; - const auto* inner_object = j.template get_ptr(); - reserve_if_possible(ret, inner_object->size(), priority_tag<1> {}); - for (const auto& p : *inner_object) - { - from_json_object_emplace(ret, p.first, p.second.template get(), priority_tag<1> {}); - } + from_json_object_impl(j, ret); obj = std::move(ret); } @@ -478,10 +603,10 @@ inline void from_json(const BasicJsonType& j, ConstructibleObjectType& obj) template < typename BasicJsonType, typename ArithmeticType, enable_if_t < std::is_arithmetic::value&& - !std::is_same::value&& - !std::is_same::value&& - !std::is_same::value&& - !std::is_same::value, + !std::is_same::number_unsigned_t>::value&& + !std::is_same::number_integer_t>::value&& + !std::is_same::number_float_t>::value&& + !std::is_same::boolean_t>::value, int > = 0 > inline void from_json(const BasicJsonType& j, ArithmeticType& val) { @@ -495,7 +620,7 @@ inline void from_json(const BasicJsonType& j, ArithmeticType& val) { throw_type_must_be("number", j); } - val = static_cast(*j.template get_ptr()); + val = static_cast(boolean_value(j)); } template @@ -505,7 +630,7 @@ detail::uncvref_t from_json_tuple_get_impl(BasicJsonType&& j, detail::iden } template::value, int> = 0> + detail::enable_if_t>, is_compatible_reference_type>::value, int> = 0> Type from_json_tuple_get_impl(BasicJsonType && j, detail::identity_tag /*unused*/, detail::priority_tag<1> /*unused*/) { return std::forward(j).template get_ref(); @@ -549,7 +674,7 @@ inline void from_json_tuple_impl(const BasicJsonType& j, std::pair& p, p template std::tuple from_json_tuple_impl(const BasicJsonType& j, identity_tag> /*unused*/, priority_tag<2> /*unused*/) { - static_assert(conjunction>, is_compatible_reference_type>...>::value, + static_assert(conjunction>, conjunction, is_compatible_reference_type>>...>::value, "Can not return a tuple containing references to types not contained in a Json, try Json::get_to()"); return from_json_tuple_impl_base<1, Args...>(j, index_sequence_for {}); } @@ -595,7 +720,7 @@ void from_json_pair_array_to_map(const BasicJsonType& j, MapType& m) // read a map with enum keys from an object, using the enum's own from_json for // the keys (e.g., from NLOHMANN_JSON_SERIALIZE_ENUM); this is the form written // with JSON_USE_OBJECTS_FOR_ENUM_KEYED_MAPS -template +template = 0> inline bool from_json_enum_keyed_object(const BasicJsonType& j, Map& m, std::true_type /*key is enum*/) { if (!j.is_object()) @@ -610,6 +735,24 @@ inline bool from_json_enum_keyed_object(const BasicJsonType& j, Map& m, std::tru return true; } +template::value, int> = 0> +inline bool from_json_enum_keyed_object(const View& v, Map& m, std::true_type /*key is enum*/) +{ + using json_t = typename View::basic_json_type; + if (!v.is_object()) + { + return false; + } + m.clear(); + for (const auto item : v.items()) + { + const auto key = item.key(); + from_json_object_assign(m, json_t(typename json_t::string_t(key.data(), key.size())).template get(), + item.value().template get(), priority_tag<1> {}); + } + return true; +} + template inline bool from_json_enum_keyed_object(const BasicJsonType& /*j*/, Map& /*m*/, std::false_type /*key is enum*/) { @@ -618,7 +761,7 @@ inline bool from_json_enum_keyed_object(const BasicJsonType& /*j*/, Map& /*m*/, template < typename BasicJsonType, typename Key, typename Value, typename Compare, typename Allocator, typename = enable_if_t < !std::is_constructible < - typename BasicJsonType::string_t, Key >::value >> + typename json_type_of_t::string_t, Key >::value >> void from_json(const BasicJsonType& j, std::map& m) { // NOLINTNEXTLINE(modernize-type-traits) we use C++11 @@ -631,7 +774,7 @@ void from_json(const BasicJsonType& j, std::map& template < typename BasicJsonType, typename Key, typename Value, typename Hash, typename KeyEqual, typename Allocator, typename = enable_if_t < !std::is_constructible < - typename BasicJsonType::string_t, Key >::value >> + typename json_type_of_t::string_t, Key >::value >> void from_json(const BasicJsonType& j, std::unordered_map& m) { // NOLINTNEXTLINE(modernize-type-traits) we use C++11 @@ -655,7 +798,7 @@ inline void from_json(const BasicJsonType& j, std_fs::path& p) { throw_type_must_be("string", j); } - const auto& s = *j.template get_ptr(); + const auto& s = string_value(j); // Checking for C++20 standard or later can be insufficient in case the // library support for char8_t is either incomplete or was disabled // altogether. Use the __cpp_lib_char8_t feature test instead. @@ -694,15 +837,14 @@ struct fn }; } // namespace from_json_adl -// the sources whose from_json() is only looked up by ADL: views (the library's -// overloads are written for basic_json) and the probe +// the sources whose from_json() is only looked up by ADL: the probe template -struct is_adl_only_from_json_source : disjunction, std::is_same> {}; +struct is_adl_only_from_json_source : std::is_same {}; struct from_json_fn { - template < typename BasicJsonType, typename T, - enable_if_t < !is_adl_only_from_json_source::value, int > = 0 > + template>::value, int> = 0> auto operator()(const BasicJsonType& j, T && val) const noexcept(noexcept(from_json(j, std::forward(val)))) -> decltype(from_json(j, std::forward(val))) @@ -710,8 +852,8 @@ struct from_json_fn return from_json(j, std::forward(val)); } - template < typename SourceType, typename T, - enable_if_t < is_adl_only_from_json_source::value, int > = 0 > + template::value, int> = 0> auto operator()(const SourceType& j, T && val) const noexcept(noexcept(from_json_adl::fn {}(j, std::forward(val)))) -> decltype(from_json_adl::fn {}(j, std::forward(val))) @@ -748,20 +890,6 @@ void from_json_base(const View& v, T& base) v.get_to(base); } -// the context of an exception thrown by a from_json() of the macros: the value -// itself for basic_json (for JSON_DIAGNOSTICS), none for a view -template::value, int> = 0> -const BasicJsonType * from_json_context(const BasicJsonType& j) noexcept -{ - return &j; -} - -template::value, int> = 0> -std::nullptr_t from_json_context(const View& /*unused*/) noexcept -{ - return nullptr; -} - // a value as an exception message shows it (invalid UTF-8 replaced; the // strings of a view are always valid UTF-8) template::value, int> = 0> diff --git a/include/nlohmann/detail/exceptions.hpp b/include/nlohmann/detail/exceptions.hpp index d47ed6f17..ee2c0feec 100644 --- a/include/nlohmann/detail/exceptions.hpp +++ b/include/nlohmann/detail/exceptions.hpp @@ -417,6 +417,20 @@ void templated_json_throw(ExceptionType exception) (void)exception; } +// the context of an exception about j: j itself for a basic_json value (whose +// path JSON_DIAGNOSTICS shows), none for a view of +template::value, int> = 0> +const BasicJsonType * exception_context(const BasicJsonType& j) noexcept +{ + return &j; +} + +template::value, int> = 0> +std::nullptr_t exception_context(const View& /*unused*/) noexcept +{ + return nullptr; +} + /*! @brief throws because @a j does not have the type a conversion expects @param[in] expected the expected type(s), e.g. "array" or "binary or array" @@ -428,7 +442,7 @@ JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE void throw_type_must_be(const cha { static_cast(expected); // unused when JSON_NOEXCEPTION is defined static_cast(j); - JSON_THROW(type_error::create(exception_id::type_mismatch, concat("type must be ", expected, ", but is ", j.type_name()), &j)); + JSON_THROW(type_error::create(exception_id::type_mismatch, concat("type must be ", expected, ", but is ", j.type_name()), exception_context(j))); } /*! diff --git a/include/nlohmann/detail/macro_scope.hpp b/include/nlohmann/detail/macro_scope.hpp index 4f89f3463..65394b504 100644 --- a/include/nlohmann/detail/macro_scope.hpp +++ b/include/nlohmann/detail/macro_scope.hpp @@ -347,7 +347,7 @@ return ej_pair.second == j; \ }); \ if (it != std::end(m)) e = it->first; \ - else ::nlohmann::detail::templated_json_throw(nlohmann::detail::out_of_range::create(nlohmann::detail::exception_id::value_out_of_range, nlohmann::detail::concat("enum value out of range for " #ENUM_TYPE ": ", nlohmann::detail::from_json_dump(j)), nlohmann::detail::from_json_context(j))); \ + else ::nlohmann::detail::templated_json_throw(nlohmann::detail::out_of_range::create(nlohmann::detail::exception_id::value_out_of_range, nlohmann::detail::concat("enum value out of range for " #ENUM_TYPE ": ", nlohmann::detail::from_json_dump(j)), nlohmann::detail::exception_context(j))); \ } // Ugly macros to avoid uglier copy-paste when specializing basic_json. They diff --git a/include/nlohmann/detail/view/value.hpp b/include/nlohmann/detail/view/value.hpp index 945071705..4b54a7785 100644 --- a/include/nlohmann/detail/view/value.hpp +++ b/include/nlohmann/detail/view/value.hpp @@ -130,14 +130,16 @@ struct is_direct_value : std::integral_constant < bool, template struct is_direct_value, BasicJsonType, View> : std::true_type {}; +// (vectors of these, which are faster than through from_json(), and containers +// of views; other containers are read by the library's from_json()) template -struct is_direct_value, BasicJsonType, View> : std::true_type {}; +struct is_direct_value, BasicJsonType, View> : is_direct_value {}; -template -struct is_direct_value, BasicJsonType, View> : is_string_key {}; +template +struct is_direct_value, BasicJsonType, View> : is_string_key {}; -template -struct is_direct_value, BasicJsonType, View> : is_string_key {}; +template +struct is_direct_value, BasicJsonType, View> : is_string_key {}; } // namespace view } // namespace detail diff --git a/include/nlohmann/json_view.hpp b/include/nlohmann/json_view.hpp index ebcb13733..071c14904 100644 --- a/include/nlohmann/json_view.hpp +++ b/include/nlohmann/json_view.hpp @@ -919,22 +919,25 @@ class basic_json_view return *this; } - template + /// vectors of the types converted directly (faster than the library's + /// from_json()) and containers of views (no from_json() reads them: a view + /// is no value of BasicJsonType); other containers are read by from_json() + template::value, int> = 0> std::vector get_impl(detail::view::value_tag> /*unused*/, detail::priority_tag<2> /*unused*/) const { return detail::view::vector_value(*this); } - template::value, int>::type = 0> - std::map get_impl(detail::view::value_tag> /*unused*/, detail::priority_tag<2> /*unused*/) const + template::value, int>::type = 0> + std::map get_impl(detail::view::value_tag> /*unused*/, detail::priority_tag<2> /*unused*/) const { - return detail::view::map_value>(*this); + return detail::view::map_value>(*this); } - template::value, int>::type = 0> - std::unordered_map get_impl(detail::view::value_tag> /*unused*/, detail::priority_tag<2> /*unused*/) const + template::value, int>::type = 0> + std::unordered_map get_impl(detail::view::value_tag> /*unused*/, detail::priority_tag<2> /*unused*/) const { - return detail::view::map_value>(*this); + return detail::view::map_value>(*this); } /// whether the from_json() of T's serializer reads views (see @@ -949,7 +952,7 @@ class basic_json_view ? 2 : static_cast(reads_view::value) >; /// types whose from_json() reads views, without materialize() - template < typename T, typename std::enable_if < reads_view::value && std::is_default_constructible::value, int >::type = 0 > + template, std::is_default_constructible>::value, int> = 0> T get_impl(detail::view::value_tag /*unused*/, detail::priority_tag<1> /*unused*/) const { auto v = T(); diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index b0492ec78..0c4753d41 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -2720,7 +2720,7 @@ JSON_HEDLEY_DIAGNOSTIC_POP return ej_pair.second == j; \ }); \ if (it != std::end(m)) e = it->first; \ - else ::nlohmann::detail::templated_json_throw(nlohmann::detail::out_of_range::create(nlohmann::detail::exception_id::value_out_of_range, nlohmann::detail::concat("enum value out of range for " #ENUM_TYPE ": ", nlohmann::detail::from_json_dump(j)), nlohmann::detail::from_json_context(j))); \ + else ::nlohmann::detail::templated_json_throw(nlohmann::detail::out_of_range::create(nlohmann::detail::exception_id::value_out_of_range, nlohmann::detail::concat("enum value out of range for " #ENUM_TYPE ": ", nlohmann::detail::from_json_dump(j)), nlohmann::detail::exception_context(j))); \ } // Ugly macros to avoid uglier copy-paste when specializing basic_json. They @@ -5914,6 +5914,20 @@ void templated_json_throw(ExceptionType exception) (void)exception; } +// the context of an exception about j: j itself for a basic_json value (whose +// path JSON_DIAGNOSTICS shows), none for a view of +template::value, int> = 0> +const BasicJsonType * exception_context(const BasicJsonType& j) noexcept +{ + return &j; +} + +template::value, int> = 0> +std::nullptr_t exception_context(const View& /*unused*/) noexcept +{ + return nullptr; +} + /*! @brief throws because @a j does not have the type a conversion expects @param[in] expected the expected type(s), e.g. "array" or "binary or array" @@ -5925,7 +5939,7 @@ JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE void throw_type_must_be(const cha { static_cast(expected); // unused when JSON_NOEXCEPTION is defined static_cast(j); - JSON_THROW(type_error::create(exception_id::type_mismatch, concat("type must be ", expected, ", but is ", j.type_name()), &j)); + JSON_THROW(type_error::create(exception_id::type_mismatch, concat("type must be ", expected, ", but is ", j.type_name()), exception_context(j))); } /*! @@ -6154,6 +6168,70 @@ NLOHMANN_JSON_NAMESPACE_BEGIN namespace detail { +// The from_json() overloads below read basic_json values and the views of +// (BasicJsonType is then a view, and +// json_type_of_t its basic_json type). The helpers below are +// the accesses that differ between the two. + +// the enable_if parameter of the overloads that read basic_json values (written +// with negation<> because astyle spaces out a template argument list with '!') +template +using enable_if_not_view_t = enable_if_t>::value, int>; + +// the boolean of j, a boolean +template = 0> +typename BasicJsonType::boolean_t boolean_value(const BasicJsonType& j) +{ + return *j.template get_ptr(); +} + +template::value, int> = 0> +bool boolean_value(const View& v) +{ + return v.template get(); +} + +// the string of j, a string +template = 0> +const typename BasicJsonType::string_t& string_value(const BasicJsonType& j) +{ + return *j.template get_ptr(); +} + +template::value, int> = 0> +typename View::string_t string_value(const View& v) +{ + const auto s = v.get_string(); + return typename View::string_t(s.data(), s.size()); +} + +// the binary value of j, a binary value (never a view: a view of a JSON text +// holds no binary values) +template = 0> +const typename BasicJsonType::binary_t& binary_value(const BasicJsonType& j) +{ + return *j.template get_ptr(); +} + +template::value, int> = 0> +typename View::basic_json_type::binary_t binary_value(const View& /*unused*/) +{ + return {}; // LCOV_EXCL_LINE +} + +// the elements of j, an array, for a range-based for loop +template = 0> +const typename BasicJsonType::array_t& array_elements(const BasicJsonType& j) +{ + return *j.template get_ptr(); +} + +template::value, int> = 0> +View array_elements(const View& v) noexcept +{ + return v; // (a view is two pointers) +} + template inline void from_json(const BasicJsonType& j, typename std::nullptr_t& n) { @@ -6182,7 +6260,7 @@ void from_json(const BasicJsonType& j, std::optional& opt) // read a number value of any of the three number types into val; // returns false (leaving val untouched) if j is not a number -template +template = 0> bool get_number_value(const BasicJsonType& j, ArithmeticType& val) { switch (static_cast(j)) @@ -6219,10 +6297,22 @@ bool get_number_value(const BasicJsonType& j, ArithmeticType& val) } } +// a view converts numbers itself (as basic_json does) +template::value, int> = 0> +bool get_number_value(const View& v, ArithmeticType& val) +{ + if (!v.is_number()) + { + return false; + } + val = v.template get(); + return true; +} + // overloads for basic_json template parameters template < typename BasicJsonType, typename ArithmeticType, enable_if_t < std::is_arithmetic::value&& - !std::is_same::value, + !std::is_same::boolean_t>::value, int > = 0 > void get_arithmetic_value(const BasicJsonType& j, ArithmeticType& val) { @@ -6233,31 +6323,31 @@ void get_arithmetic_value(const BasicJsonType& j, ArithmeticType& val) } template -inline void from_json(const BasicJsonType& j, typename BasicJsonType::boolean_t& b) +inline void from_json(const BasicJsonType& j, typename json_type_of_t::boolean_t& b) { if (JSON_HEDLEY_UNLIKELY(!j.is_boolean())) { throw_type_must_be("boolean", j); } - b = *j.template get_ptr(); + b = boolean_value(j); } template -inline void from_json(const BasicJsonType& j, typename BasicJsonType::string_t& s) +inline void from_json(const BasicJsonType& j, typename json_type_of_t::string_t& s) { if (JSON_HEDLEY_UNLIKELY(!j.is_string())) { throw_type_must_be("string", j); } - s = *j.template get_ptr(); + s = string_value(j); } template < typename BasicJsonType, typename StringType, enable_if_t < - std::is_assignable::value - && is_detected_exact::value - && !std::is_same::value + std::is_assignable::string_t>::value + && is_detected_exact::string_t::value_type, value_type_t, StringType>::value + && !std::is_same::string_t, StringType>::value && !is_json_ref::value, int > = 0 > inline void from_json(const BasicJsonType& j, StringType& s) { @@ -6266,23 +6356,23 @@ inline void from_json(const BasicJsonType& j, StringType& s) throw_type_must_be("string", j); } - s = *j.template get_ptr(); + s = string_value(j); } template -inline void from_json(const BasicJsonType& j, typename BasicJsonType::number_float_t& val) +inline void from_json(const BasicJsonType& j, typename json_type_of_t::number_float_t& val) { get_arithmetic_value(j, val); } template -inline void from_json(const BasicJsonType& j, number_unsigned_overload_t& val) +inline void from_json(const BasicJsonType& j, number_unsigned_overload_t>& val) { get_arithmetic_value(j, val); } template -inline void from_json(const BasicJsonType& j, typename BasicJsonType::number_integer_t& val) +inline void from_json(const BasicJsonType& j, typename json_type_of_t::number_integer_t& val) { get_arithmetic_value(j, val); } @@ -6294,8 +6384,8 @@ inline void from_json(const BasicJsonType& j, EnumType& e) { using underlying_type = typename std::underlying_type::type; // get_arithmetic_value() does not accept boolean_t; read the number that to_json() wrote instead - using value_type = typename std::conditional::value, - typename BasicJsonType::number_unsigned_t, underlying_type>::type; + using value_type = typename std::conditional::boolean_t>::value, + typename json_type_of_t::number_unsigned_t, underlying_type>::type; value_type val; get_arithmetic_value(j, val); e = static_cast(bool_aware_static_cast(val)); @@ -6304,7 +6394,7 @@ inline void from_json(const BasicJsonType& j, EnumType& e) // forward_list doesn't have an insert method template::value, int> = 0> + enable_if_t, T>::value, int> = 0> inline void from_json(const BasicJsonType& j, std::forward_list& l) { if (JSON_HEDLEY_UNLIKELY(!j.is_array())) @@ -6312,16 +6402,16 @@ inline void from_json(const BasicJsonType& j, std::forward_list& l throw_type_must_be("array", j); } l.clear(); - std::transform(j.rbegin(), j.rend(), - std::front_inserter(l), [](const BasicJsonType & i) + auto pos = l.before_begin(); + for (const auto& elem : array_elements(j)) { - return i.template get(); - }); + pos = l.insert_after(pos, elem.template get()); + } } // valarray doesn't have an insert method template::value, int> = 0> + enable_if_t, T>::value, int> = 0> inline void from_json(const BasicJsonType& j, std::valarray& l) { if (JSON_HEDLEY_UNLIKELY(!j.is_array())) @@ -6329,11 +6419,11 @@ inline void from_json(const BasicJsonType& j, std::valarray& l) throw_type_must_be("array", j); } l.resize(j.size()); - std::transform(j.begin(), j.end(), std::begin(l), - [](const BasicJsonType & elem) + std::size_t i = 0; + for (const auto& elem : array_elements(j)) { - return elem.template get(); - }); + l[i++] = elem.template get(); + } } // element is not itself a C array: read it directly @@ -6361,7 +6451,8 @@ auto from_json(const BasicJsonType& j, T (&arr)[N]) // NOLINT(cppcoreguidelines from_json_c_array_element(j, arr); } -template +// (for a view, array_t is read element by element below) +template = 0> inline void from_json_array_impl(const BasicJsonType& j, typename BasicJsonType::array_t& arr, priority_tag<3> /*unused*/) { arr = *j.template get_ptr(); @@ -6370,7 +6461,7 @@ inline void from_json_array_impl(const BasicJsonType& j, typename BasicJsonType: // fill a std::array from the JSON array j: the size is checked once and the // elements are read in order, so a short array converts the elements it has and // then throws the out_of_range error at() would throw for the first missing one -template +template = 0> void from_json_std_array(const BasicJsonType& j, std::array& arr) { const auto& inner_array = *j.template get_ptr(); @@ -6385,6 +6476,21 @@ void from_json_std_array(const BasicJsonType& j, std::array& arr) } } +// (a view has no random access to the elements: they are read in order) +template::value, int> = 0> +void from_json_std_array(const View& v, std::array& arr) +{ + std::size_t size = 0; + for (auto it = v.begin(); size < N && it != v.end(); ++it) + { + arr[size++] = (*it).template get(); + } + if (JSON_HEDLEY_UNLIKELY(size < N)) + { + static_cast(v.at(size)); + } +} + template auto from_json_array_impl(const BasicJsonType& j, std::array& arr, priority_tag<2> /*unused*/) @@ -6433,9 +6539,8 @@ auto from_json_array_impl(const BasicJsonType& j, ConstructibleArrayType& arr, p void()) { ConstructibleArrayType ret; - const auto* inner_array = j.template get_ptr(); - reserve_if_possible(ret, inner_array->size(), priority_tag<1> {}); - for (const auto& elem : *inner_array) + reserve_if_possible(ret, j.size(), priority_tag<1> {}); + for (const auto& elem : array_elements(j)) { // get() returns *this, this won't call a from_json // method when value_type is BasicJsonType @@ -6446,11 +6551,11 @@ auto from_json_array_impl(const BasicJsonType& j, ConstructibleArrayType& arr, p template < typename BasicJsonType, typename ConstructibleArrayType, enable_if_t < - is_constructible_array_type::value&& - !is_constructible_object_type::value&& - !is_constructible_string_type::value&& - !std::is_same::value&& - !is_compatible_binary_type::value&& + is_constructible_array_type, ConstructibleArrayType>::value&& + !is_constructible_object_type, ConstructibleArrayType>::value&& + !is_constructible_string_type, ConstructibleArrayType>::value&& + !std::is_same::binary_t>::value&& + !is_compatible_binary_type, ConstructibleArrayType>::value&& !is_basic_json::value, int > = 0 > auto from_json(const BasicJsonType& j, ConstructibleArrayType& arr) @@ -6517,24 +6622,24 @@ auto from_json(const BasicJsonType& j, identity_tag> tag) } template -inline void from_json(const BasicJsonType& j, typename BasicJsonType::binary_t& bin) +inline void from_json(const BasicJsonType& j, typename json_type_of_t::binary_t& bin) { if (JSON_HEDLEY_UNLIKELY(!j.is_binary())) { throw_type_must_be("binary", j); } - bin = *j.template get_ptr(); + bin = binary_value(j); } template < typename BasicJsonType, typename CompatibleArrayType, - enable_if_t < is_compatible_binary_type::value, + enable_if_t < is_compatible_binary_type, CompatibleArrayType>::value, int > = 0 > inline void from_json(const BasicJsonType& j, CompatibleArrayType& bin) { if (j.is_binary()) { - bin = static_cast(*j.template get_ptr()); + bin = static_cast(binary_value(j)); } else if (j.is_array()) { @@ -6563,8 +6668,47 @@ void from_json_object_emplace(ConstructibleObjectType& obj, Key&& key, Value&& v obj.emplace(std::forward(key), std::forward(value)); } +// a member of a view's object: a later member with the same key replaces an +// earlier one, as parse() keeps the last value of a repeated key (containers +// without operator[] keep the first) +template +auto from_json_object_assign(ConstructibleObjectType& obj, Key&& key, Value&& value, priority_tag<1> /*unused*/) +-> decltype(obj[std::forward(key)] = std::forward(value), void()) +{ + obj[std::forward(key)] = std::forward(value); +} + +template +void from_json_object_assign(ConstructibleObjectType& obj, Key&& key, Value&& value, priority_tag<0> /*unused*/) +{ + obj.emplace(std::forward(key), std::forward(value)); +} + +template = 0> +void from_json_object_impl(const BasicJsonType& j, ConstructibleObjectType& ret) +{ + const auto* inner_object = j.template get_ptr(); + reserve_if_possible(ret, inner_object->size(), priority_tag<1> {}); + for (const auto& p : *inner_object) + { + from_json_object_emplace(ret, p.first, p.second.template get(), priority_tag<1> {}); + } +} + +template::value, int> = 0> +void from_json_object_impl(const View& v, ConstructibleObjectType& ret) +{ + using string_t = typename View::basic_json_type::object_t::key_type; + reserve_if_possible(ret, v.size(), priority_tag<1> {}); + for (const auto item : v.items()) + { + const auto key = item.key(); + from_json_object_assign(ret, string_t(key.data(), key.size()), item.value().template get(), priority_tag<1> {}); + } +} + template::value, int> = 0> + enable_if_t, ConstructibleObjectType>::value, int> = 0> inline void from_json(const BasicJsonType& j, ConstructibleObjectType& obj) { if (JSON_HEDLEY_UNLIKELY(!j.is_object())) @@ -6573,12 +6717,7 @@ inline void from_json(const BasicJsonType& j, ConstructibleObjectType& obj) } ConstructibleObjectType ret; - const auto* inner_object = j.template get_ptr(); - reserve_if_possible(ret, inner_object->size(), priority_tag<1> {}); - for (const auto& p : *inner_object) - { - from_json_object_emplace(ret, p.first, p.second.template get(), priority_tag<1> {}); - } + from_json_object_impl(j, ret); obj = std::move(ret); } @@ -6587,10 +6726,10 @@ inline void from_json(const BasicJsonType& j, ConstructibleObjectType& obj) template < typename BasicJsonType, typename ArithmeticType, enable_if_t < std::is_arithmetic::value&& - !std::is_same::value&& - !std::is_same::value&& - !std::is_same::value&& - !std::is_same::value, + !std::is_same::number_unsigned_t>::value&& + !std::is_same::number_integer_t>::value&& + !std::is_same::number_float_t>::value&& + !std::is_same::boolean_t>::value, int > = 0 > inline void from_json(const BasicJsonType& j, ArithmeticType& val) { @@ -6604,7 +6743,7 @@ inline void from_json(const BasicJsonType& j, ArithmeticType& val) { throw_type_must_be("number", j); } - val = static_cast(*j.template get_ptr()); + val = static_cast(boolean_value(j)); } template @@ -6614,7 +6753,7 @@ detail::uncvref_t from_json_tuple_get_impl(BasicJsonType&& j, detail::iden } template::value, int> = 0> + detail::enable_if_t>, is_compatible_reference_type>::value, int> = 0> Type from_json_tuple_get_impl(BasicJsonType && j, detail::identity_tag /*unused*/, detail::priority_tag<1> /*unused*/) { return std::forward(j).template get_ref(); @@ -6658,7 +6797,7 @@ inline void from_json_tuple_impl(const BasicJsonType& j, std::pair& p, p template std::tuple from_json_tuple_impl(const BasicJsonType& j, identity_tag> /*unused*/, priority_tag<2> /*unused*/) { - static_assert(conjunction>, is_compatible_reference_type>...>::value, + static_assert(conjunction>, conjunction, is_compatible_reference_type>>...>::value, "Can not return a tuple containing references to types not contained in a Json, try Json::get_to()"); return from_json_tuple_impl_base<1, Args...>(j, index_sequence_for {}); } @@ -6704,7 +6843,7 @@ void from_json_pair_array_to_map(const BasicJsonType& j, MapType& m) // read a map with enum keys from an object, using the enum's own from_json for // the keys (e.g., from NLOHMANN_JSON_SERIALIZE_ENUM); this is the form written // with JSON_USE_OBJECTS_FOR_ENUM_KEYED_MAPS -template +template = 0> inline bool from_json_enum_keyed_object(const BasicJsonType& j, Map& m, std::true_type /*key is enum*/) { if (!j.is_object()) @@ -6719,6 +6858,24 @@ inline bool from_json_enum_keyed_object(const BasicJsonType& j, Map& m, std::tru return true; } +template::value, int> = 0> +inline bool from_json_enum_keyed_object(const View& v, Map& m, std::true_type /*key is enum*/) +{ + using json_t = typename View::basic_json_type; + if (!v.is_object()) + { + return false; + } + m.clear(); + for (const auto item : v.items()) + { + const auto key = item.key(); + from_json_object_assign(m, json_t(typename json_t::string_t(key.data(), key.size())).template get(), + item.value().template get(), priority_tag<1> {}); + } + return true; +} + template inline bool from_json_enum_keyed_object(const BasicJsonType& /*j*/, Map& /*m*/, std::false_type /*key is enum*/) { @@ -6727,7 +6884,7 @@ inline bool from_json_enum_keyed_object(const BasicJsonType& /*j*/, Map& /*m*/, template < typename BasicJsonType, typename Key, typename Value, typename Compare, typename Allocator, typename = enable_if_t < !std::is_constructible < - typename BasicJsonType::string_t, Key >::value >> + typename json_type_of_t::string_t, Key >::value >> void from_json(const BasicJsonType& j, std::map& m) { // NOLINTNEXTLINE(modernize-type-traits) we use C++11 @@ -6740,7 +6897,7 @@ void from_json(const BasicJsonType& j, std::map& template < typename BasicJsonType, typename Key, typename Value, typename Hash, typename KeyEqual, typename Allocator, typename = enable_if_t < !std::is_constructible < - typename BasicJsonType::string_t, Key >::value >> + typename json_type_of_t::string_t, Key >::value >> void from_json(const BasicJsonType& j, std::unordered_map& m) { // NOLINTNEXTLINE(modernize-type-traits) we use C++11 @@ -6764,7 +6921,7 @@ inline void from_json(const BasicJsonType& j, std_fs::path& p) { throw_type_must_be("string", j); } - const auto& s = *j.template get_ptr(); + const auto& s = string_value(j); // Checking for C++20 standard or later can be insufficient in case the // library support for char8_t is either incomplete or was disabled // altogether. Use the __cpp_lib_char8_t feature test instead. @@ -6803,15 +6960,14 @@ struct fn }; } // namespace from_json_adl -// the sources whose from_json() is only looked up by ADL: views (the library's -// overloads are written for basic_json) and the probe +// the sources whose from_json() is only looked up by ADL: the probe template -struct is_adl_only_from_json_source : disjunction, std::is_same> {}; +struct is_adl_only_from_json_source : std::is_same {}; struct from_json_fn { - template < typename BasicJsonType, typename T, - enable_if_t < !is_adl_only_from_json_source::value, int > = 0 > + template>::value, int> = 0> auto operator()(const BasicJsonType& j, T && val) const noexcept(noexcept(from_json(j, std::forward(val)))) -> decltype(from_json(j, std::forward(val))) @@ -6819,8 +6975,8 @@ struct from_json_fn return from_json(j, std::forward(val)); } - template < typename SourceType, typename T, - enable_if_t < is_adl_only_from_json_source::value, int > = 0 > + template::value, int> = 0> auto operator()(const SourceType& j, T && val) const noexcept(noexcept(from_json_adl::fn {}(j, std::forward(val)))) -> decltype(from_json_adl::fn {}(j, std::forward(val))) @@ -6857,20 +7013,6 @@ void from_json_base(const View& v, T& base) v.get_to(base); } -// the context of an exception thrown by a from_json() of the macros: the value -// itself for basic_json (for JSON_DIAGNOSTICS), none for a view -template::value, int> = 0> -const BasicJsonType * from_json_context(const BasicJsonType& j) noexcept -{ - return &j; -} - -template::value, int> = 0> -std::nullptr_t from_json_context(const View& /*unused*/) noexcept -{ - return nullptr; -} - // a value as an exception message shows it (invalid UTF-8 replaced; the // strings of a view are always valid UTF-8) template::value, int> = 0> diff --git a/single_include/nlohmann/json_view.hpp b/single_include/nlohmann/json_view.hpp index 44a7c62d9..e611ff540 100644 --- a/single_include/nlohmann/json_view.hpp +++ b/single_include/nlohmann/json_view.hpp @@ -7822,14 +7822,16 @@ struct is_direct_value : std::integral_constant < bool, template struct is_direct_value, BasicJsonType, View> : std::true_type {}; +// (vectors of these, which are faster than through from_json(), and containers +// of views; other containers are read by the library's from_json()) template -struct is_direct_value, BasicJsonType, View> : std::true_type {}; +struct is_direct_value, BasicJsonType, View> : is_direct_value {}; -template -struct is_direct_value, BasicJsonType, View> : is_string_key {}; +template +struct is_direct_value, BasicJsonType, View> : is_string_key {}; -template -struct is_direct_value, BasicJsonType, View> : is_string_key {}; +template +struct is_direct_value, BasicJsonType, View> : is_string_key {}; } // namespace view } // namespace detail @@ -8671,22 +8673,25 @@ class basic_json_view return *this; } - template + /// vectors of the types converted directly (faster than the library's + /// from_json()) and containers of views (no from_json() reads them: a view + /// is no value of BasicJsonType); other containers are read by from_json() + template::value, int> = 0> std::vector get_impl(detail::view::value_tag> /*unused*/, detail::priority_tag<2> /*unused*/) const { return detail::view::vector_value(*this); } - template::value, int>::type = 0> - std::map get_impl(detail::view::value_tag> /*unused*/, detail::priority_tag<2> /*unused*/) const + template::value, int>::type = 0> + std::map get_impl(detail::view::value_tag> /*unused*/, detail::priority_tag<2> /*unused*/) const { - return detail::view::map_value>(*this); + return detail::view::map_value>(*this); } - template::value, int>::type = 0> - std::unordered_map get_impl(detail::view::value_tag> /*unused*/, detail::priority_tag<2> /*unused*/) const + template::value, int>::type = 0> + std::unordered_map get_impl(detail::view::value_tag> /*unused*/, detail::priority_tag<2> /*unused*/) const { - return detail::view::map_value>(*this); + return detail::view::map_value>(*this); } /// whether the from_json() of T's serializer reads views (see @@ -8701,7 +8706,7 @@ class basic_json_view ? 2 : static_cast(reads_view::value) >; /// types whose from_json() reads views, without materialize() - template < typename T, typename std::enable_if < reads_view::value && std::is_default_constructible::value, int >::type = 0 > + template, std::is_default_constructible>::value, int> = 0> T get_impl(detail::view::value_tag /*unused*/, detail::priority_tag<1> /*unused*/) const { auto v = T(); diff --git a/tests/src/unit-json_view_from_json.cpp b/tests/src/unit-json_view_from_json.cpp index f106b1fe0..ec296e5db 100644 --- a/tests/src/unit-json_view_from_json.cpp +++ b/tests/src/unit-json_view_from_json.cpp @@ -52,6 +52,11 @@ struct point int x = 0; int y = 0; NLOHMANN_DEFINE_TYPE_INTRUSIVE(point, x, y) + + friend bool operator==(const point& a, const point& b) + { + return a.x == b.x && a.y == b.y; + } }; struct shape @@ -184,6 +189,13 @@ namespace { template struct reads_view : nlohmann::detail::has_view_from_json, View, T> {}; + +// whether a member converts to the same T from the view and from the materialized value +template +bool same(const json_view& v, const json& j, const char* key) +{ + return v[key].get() == j[key].get(); +} } // namespace TEST_CASE("json_view from_json") @@ -215,39 +227,119 @@ TEST_CASE("json_view from_json") static_assert(!nlohmann::is_json_view::value, "is_json_view"); } - SECTION("the library's own conversions are not called with a view (and the probe compiles for them)") + SECTION("the library's own conversions read views") { - // these go through materialize() (vectors, maps with string keys, - // strings, and numbers are converted by the view itself) - static_assert(!reads_view>::value, "std types"); - static_assert(!reads_view>::value, "std types"); - static_assert(!reads_view>::value, "std types"); - static_assert(!reads_view>::value, "std types"); - static_assert(!reads_view>::value, "std types"); - static_assert(!reads_view>::value, "std types"); - static_assert(!reads_view>::value, "std types"); - static_assert(!reads_view>::value, "std types"); - static_assert(!reads_view>::value, "std types"); - static_assert(!reads_view>::value, "std types"); - static_assert(!reads_view>::value, "std types"); - static_assert(!reads_view>::value, "std types"); - static_assert(!reads_view>::value, "std types"); - static_assert(!reads_view>::value, "std types"); - static_assert(!reads_view>::value, "std types"); - static_assert(!reads_view>::value, "std types"); - static_assert(!reads_view::value, "std types"); - static_assert(!reads_view::value, "std types"); - static_assert(!reads_view::value, "std types"); - static_assert(!reads_view::value, "std types"); - static_assert(!reads_view::value, "std types"); - static_assert(!reads_view::value, "std types"); - static_assert(!reads_view::value, "std types"); + static_assert(reads_view>::value, "std types"); + static_assert(reads_view>::value, "std types"); + static_assert(reads_view>::value, "std types"); + static_assert(reads_view>::value, "std types"); + static_assert(reads_view>::value, "std types"); + static_assert(reads_view>::value, "std types"); + static_assert(reads_view>::value, "std types"); + static_assert(reads_view>::value, "std types"); + static_assert(reads_view>::value, "std types"); + static_assert(reads_view>::value, "std types"); + static_assert(reads_view>::value, "std types"); + static_assert(reads_view>::value, "std types"); + static_assert(reads_view>::value, "std types"); + static_assert(reads_view>::value, "std types"); + static_assert(reads_view>::value, "std types"); + static_assert(reads_view>::value, "std types"); + static_assert(reads_view::value, "std types"); + static_assert(reads_view::value, "std types"); + static_assert(reads_view::value, "std types"); + static_assert(reads_view::value, "std types"); + static_assert(reads_view::value, "std types"); + static_assert(!reads_view::value, "get() is materialize()"); + static_assert(reads_view::value, "std types"); #ifdef JSON_HAS_CPP_17 - static_assert(!reads_view>::value, "std types"); + static_assert(reads_view>::value, "std types"); #endif CHECK(true); } + SECTION("std types: the same values as from the materialized value") + { + const json_document d = json_document::parse(R"({"ints": [3, 1, 2, 1], "strings": ["b", "a", "b"], "pairs": [[1, "x"], [2, "y"]], + "points": [{"x": 1, "y": 2}, {"x": 3, "y": 4}], "object": {"b": 1, "a": 2}, "triple": [1, true, {"x": 5, "y": 6}], + "colors": {"green": 1, "red": 2}, "bytes": [1, 2, 255], "nested": [[1, 2], [3]], "number": 1.5, "flag": true, + "text": "hello", "nothing": null, "mixed": [1, 2.5, -3, 4], "flags": [true, false, true]})"); + const json j = d.root().materialize(); + const json_view v = d.root(); + CHECK(same>(v, j, "ints")); + CHECK(same>(v, j, "ints")); + CHECK(same>(v, j, "ints")); + CHECK(same>(v, j, "ints")); + CHECK(same>(v, j, "strings")); + CHECK(same>(v, j, "ints")); + CHECK(same>(v, j, "ints")); + CHECK(same>(v, j, "mixed")); + CHECK((v["pairs"][0].get>() == j["pairs"][0].get>())); + CHECK(same>>(v, j, "pairs")); + CHECK(same>(v, j, "triple")); + CHECK(same>(v, j, "pairs")); + CHECK(same>(v, j, "pairs")); + CHECK(same>(v, j, "object")); + CHECK(same>(v, j, "object")); + CHECK(same>(v, j, "colors")); + CHECK(same>(v, j, "points")); + CHECK(same>(v, j, "ints")); + CHECK(same>(v, j, "flags")); + CHECK(same>(v, j, "bytes")); + CHECK(same>>(v, j, "nested")); + CHECK(same>(v, j, "mixed")); + CHECK(same(v, j, "number")); + CHECK(same(v, j, "flag")); + CHECK(same(v, j, "text")); + CHECK(same(v, j, "nothing")); + CHECK(same(v, j, "object")); + const std::valarray va = v["ints"].get>(); + CHECK(va.size() == 4); + CHECK(va[0] == 3); + int c_array[2][2] = {{0, 0}, {0, 0}}; // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays): the conversion to C arrays + v["nested"][0].get_to(c_array[0]); + CHECK(c_array[0][1] == 2); +#ifdef JSON_HAS_CPP_17 + CHECK(v["points"][0].get>()->y == 2); + CHECK(!v["nothing"].get>().has_value()); +#endif + // containers of views keep views of the elements + const std::vector views = v["points"].get>(); + CHECK(views.size() == 2); + CHECK(views[1]["x"].get() == 3); + CHECK(v["object"].get>().at("a").get() == 2); + } + + SECTION("std types: duplicate keys keep the last value, as from the materialized value") + { + const json_document d = json_document::parse(R"({"a": 1, "b": 2, "a": 3})"); + CHECK((d.root().get>() == d.root().materialize().get>())); + CHECK(d.root().get>().at("a") == 3); + CHECK(d.root().get>().at("a") == 3); + } + +#if !defined(JSON_NOEXCEPTION) && !JSON_DIAGNOSTICS + SECTION("std types: the same errors as from the materialized value") + { + const json_document d = json_document::parse(R"([[1, "x"], {"a": 1}, "text", [1], 5, null])"); + const json j = d.root().materialize(); + for (std::size_t i = 0; i < 6; ++i) + { + CAPTURE(i) + const json_view v = d.root()[i]; + CHECK(exception_of_call([&] { static_cast(v.get>()); }) == exception_of_call([&] { static_cast(j[i].get>()); })); + CHECK(exception_of_call([&] { static_cast(v.get>()); }) == exception_of_call([&] { static_cast(j[i].get>()); })); + CHECK(exception_of_call([&] { static_cast(v.get>()); }) == exception_of_call([&] { static_cast(j[i].get>()); })); + CHECK(exception_of_call([&] { static_cast(v.get>()); }) == exception_of_call([&] { static_cast(j[i].get>()); })); + CHECK(exception_of_call([&] { static_cast(v.get>()); }) == exception_of_call([&] { static_cast(j[i].get>()); })); + CHECK(exception_of_call([&] { static_cast(v.get>()); }) == exception_of_call([&] { static_cast(j[i].get>()); })); + CHECK(exception_of_call([&] { static_cast(v.get>()); }) == exception_of_call([&] { static_cast(j[i].get>()); })); + CHECK(exception_of_call([&] { static_cast(v.get()); }) == exception_of_call([&] { static_cast(j[i].get()); })); + CHECK(exception_of_call([&] { static_cast(v.get>()); }) == exception_of_call([&] { static_cast(j[i].get>()); })); + } + } +#endif + SECTION("macro types") { const json_document d = json_document::parse(R"({"name": "s", "points": [{"x": 1, "y": 2}, {"x": 3, "y": 4}], From 94fe31eb9b94debcae52bc0d493a2232c11dbeaf Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Mon, 26 Oct 2026 20:13:32 +0100 Subject: [PATCH 18/18] docs: writing generic code for json_view (#5924) * docs: writing generic code for json_view A "Writing generic code" section on the json_view page: why a function taking json_view accepts no other view, the three signatures for editable, read-only, or either kind of view (passed by value), why the modes do not convert, is_json_view, and how a from_json() reads views (constrained to json values and views; the macros and the library's conversions do it already) with what differs then (duplicate keys, JSON_DIAGNOSTICS paths). Signed-off-by: Niels Lohmann * docs: name the documents the generic view function accepts Signed-off-by: Niels Lohmann * docs: views of read-only documents convert to editable views Signed-off-by: Niels Lohmann --------- Signed-off-by: Niels Lohmann --- docs/mkdocs/docs/api/basic_json_view/get.md | 2 +- docs/mkdocs/docs/api/is_json_view.md | 1 + docs/mkdocs/docs/api/view_mode.md | 3 +- docs/mkdocs/docs/features/json_view.md | 100 ++++++++++++++++++-- 4 files changed, 98 insertions(+), 8 deletions(-) diff --git a/docs/mkdocs/docs/api/basic_json_view/get.md b/docs/mkdocs/docs/api/basic_json_view/get.md index cd0a98bff..191fabdf5 100644 --- a/docs/mkdocs/docs/api/basic_json_view/get.md +++ b/docs/mkdocs/docs/api/basic_json_view/get.md @@ -38,7 +38,7 @@ as [`BasicJsonType::parse()`](../basic_json/parse.md) does), as well as the type accept views, e.g. with [`is_json_view`](../is_json_view.md). A `from_json()` that also accepts types that are no JSON values at all, such as the unconstrained `#!cpp template void from_json(const BasicJsonType&, T&)` of [arbitrary type conversions](../../features/arbitrary_types.md), is not called with a view: it may use -`BasicJsonType` functions a view does not have. See [json_view](../../features/json_view.md). +`BasicJsonType` functions a view does not have. See [Writing generic code](../../features/json_view.md#writing-generic-code). !!! warning "Duplicate keys" diff --git a/docs/mkdocs/docs/api/is_json_view.md b/docs/mkdocs/docs/api/is_json_view.md index 53bd8ec3a..6153adf30 100644 --- a/docs/mkdocs/docs/api/is_json_view.md +++ b/docs/mkdocs/docs/api/is_json_view.md @@ -49,6 +49,7 @@ view class. - [basic_json_view](basic_json_view/index.md) - a handle to one value of a document - [get](basic_json_view/get.md) - which conversions read the view directly - [view_mode](view_mode.md) - read-only or editable +- [Writing generic code](../features/json_view.md#writing-generic-code) - functions and `from_json()` for views ## Version history diff --git a/docs/mkdocs/docs/api/view_mode.md b/docs/mkdocs/docs/api/view_mode.md index c0339b5fb..c754c7ed4 100644 --- a/docs/mkdocs/docs/api/view_mode.md +++ b/docs/mkdocs/docs/api/view_mode.md @@ -50,7 +50,7 @@ void f(basic_json_view v); template void f(basic_json_view v); -// either +// views of either document type template void f(basic_json_view v); ``` @@ -82,6 +82,7 @@ nor a [`json_editable_view`](json_editable_view.md). Views of the two modes comp - [basic_json_view](basic_json_view/index.md) - a handle to one value of a document - [json_editable_document](json_editable_document.md) - an editable document of `json` - [(constructor)](basic_json_view/basic_json_view.md) - the conversion from a read-only to an editable view +- [Writing generic code](../features/json_view.md#writing-generic-code) - functions that accept views ## Version history diff --git a/docs/mkdocs/docs/features/json_view.md b/docs/mkdocs/docs/features/json_view.md index 7de4a07cd..bef6259de 100644 --- a/docs/mkdocs/docs/features/json_view.md +++ b/docs/mkdocs/docs/features/json_view.md @@ -214,14 +214,15 @@ number of distinct keys, or reject duplicates when iterating. ## Getting values out without copying -[`get()`](../api/basic_json_view/get.md) converts many `T` directly from the flat index, without ever building a +[`get()`](../api/basic_json_view/get.md) converts most `T` directly from the flat index, without ever building a `basic_json` value for the conversion: `#!cpp bool`, arithmetic types, `#!cpp std::nullptr_t`, -`#!cpp std::string`/other `#!cpp std::basic_string`s (copied once), `basic_json`/`ordered_json` (via -[`materialize()`](../api/basic_json_view/materialize.md)), `basic_json_view` itself, `#!cpp std::vector`, and -`#!cpp std::map`/`#!cpp std::unordered_map` with string-like keys. Every other type -- `#!cpp std::list`, -`#!cpp std::pair`, `#!cpp std::array`, enumerations, user types with a `from_json()` -- goes through +`#!cpp std::string`/other `#!cpp std::basic_string`s (copied once), `basic_json_view` itself, the standard containers +and other types the library converts (`#!cpp std::vector`, `#!cpp std::map`, `#!cpp std::list`, `#!cpp std::array`, +`#!cpp std::pair`, `#!cpp std::tuple`, `#!cpp std::optional`, ...), and the types of the `NLOHMANN_DEFINE_TYPE_*` and +`NLOHMANN_JSON_SERIALIZE_ENUM` macros. Types whose `from_json()` is written for `basic_json` only go through [`materialize()`](../api/basic_json_view/materialize.md)`.get()` instead: the subtree is built into a real -`basic_json` value first, exactly as [`parse()`](../api/basic_json/parse.md) would, and converted from there. +`basic_json` value first, exactly as [`parse()`](../api/basic_json/parse.md) would, and converted from there. See +[Writing generic code](#writing-generic-code) for how to write a `from_json()` that reads views. Two conversions never copy at all: @@ -236,6 +237,93 @@ Two conversions never copy at all: Both results are only valid as long as the view -- and, for a string with no escapes, the borrowed source text -- is. +## Writing generic code + +A view's type names its `BasicJsonType` and its [`view_mode`](../api/view_mode.md): +[`json_view`](../api/json_view.md) is `#!cpp basic_json_view`, and +[`json_editable_view`](../api/json_editable_view.md) is `#!cpp basic_json_view`. These are +different types. The only conversion between them turns a view of a read-only document into a view of an editable +document of the same `BasicJsonType`, so a function written for one shortcut accepts few other views: + +```cpp +void print_names(json_view v); // accepts neither an ordered_json_view nor a json_editable_view +void print_names(json_editable_view v); // also accepts a json_view, but no ordered_json_view +``` + +This is the same pitfall as a `to_json`/`from_json` written for `json` instead of any `BasicJsonType`, which then does +not work for `ordered_json`. The shortcuts are for declaring documents and views; a function that takes a view names +the views it accepts with a template: + +```cpp +// views of editable documents only +template +void f(basic_json_view v); + +// views of read-only documents only +template +void f(basic_json_view v); + +// views of either document type +template +void f(basic_json_view v); +``` + +Views are passed by value: they are two pointers, cheap to copy, like a `#!cpp std::string_view`. A `const&` works as +well, but a non-`const` reference does not bind to the views that [`root()`](../api/basic_json_document/root.md), +[`operator[]`](../api/basic_json_view/operator%5B%5D.md), and [`at()`](../api/basic_json_view/at.md) return, so +`#!cpp f(doc.root())` would not compile. + +There is no conversion from an editable view to a read-only one, by design: the mode decides how a view walks the +index. A read-only view reads a container in place, while a view of an editable document follows the links that edits +leave behind, so a read-only view of an edited document would read stale values. The other direction is safe: a +read-only document has no edits, so a view of it [converts](../api/basic_json_view/basic_json_view.md) to an +editable view, which reads it like an editable document before its first edit. A template deduces the mode without +conversions, so the template forms above still accept only the modes they name. Views of both modes compare with +[`operator==`](../api/basic_json_view/operator_eq.md). + +[`is_json_view`](../api/is_json_view.md) tells views from other types, for constraints that cannot be written as a +`basic_json_view<...>` parameter. + +### A `from_json()` that reads views + +[`get()`](../api/basic_json_view/get.md) on a view calls `T`'s `from_json()` with the view itself if that +`from_json()` accepts views, and with [`materialize()`](../api/basic_json_view/materialize.md) otherwise. The +`from_json()` functions of the [`NLOHMANN_DEFINE_TYPE_*`](../api/macros/nlohmann_define_type_intrusive.md), +[`NLOHMANN_DEFINE_DERIVED_TYPE_*`](../api/macros/nlohmann_define_derived_type.md), and +[`NLOHMANN_JSON_SERIALIZE_ENUM`](../api/macros/nlohmann_json_serialize_enum.md) macros, and those of the library +for the standard types, accept views, so these types need nothing else. For decoding the statuses of +twitter.json into structs, reading the view is about 5 times faster than `#!cpp materialize().get()`. + +A hand-written `from_json()` reads views if it is constrained to `json` values and views: + +```cpp +template < typename J, typename std::enable_if < + nlohmann::is_json_view::value || std::is_same::value, int >::type = 0 > +void from_json(const J& j, person& p) +{ + p.name = j.at("name").template get(); + p.age = j.at("age").template get(); +} +``` + +Its body may only use what views and `basic_json` have in common: the type checks, `at()`, `operator[]`, `contains()`, +`value()`, `size()`, iteration and `items()`, and `get()`/`get_to()`. An unconstrained +`#!cpp template void from_json(const BasicJsonType& j, person& p)`, as in +[arbitrary type conversions](arbitrary_types.md), is never called with a view, as it might use `basic_json` functions +a view does not have; such a type is converted through `materialize()`. A `from_json()` for `json` only is +converted through `materialize()` as well. + +Two things differ when a `from_json()` reads the view: + +- **Duplicate keys.** Members looked up by name (`at()`, `value()`, ... and thus the macros) find the **first** member + with a repeated key, see [Duplicate keys](#duplicate-keys); `#!cpp materialize().get()` would see the last one. + Containers built from an object, such as `#!cpp std::map`, keep the **last** value either way, as + [`parse()`](../api/basic_json/parse.md) does. +- **[`JSON_DIAGNOSTICS`](../api/macros/json_diagnostics.md).** Exceptions thrown by the view carry no path to the + value. + +`to_json()` is unaffected: it writes `basic_json` values, never views. + ## Writing a view back [`dump()`](../api/basic_json_view/dump.md) serializes a view directly from the flat index, without ever building a
JSONBytesMeaning
{8Aobject with 4 members
"id": 300,69 64 C4 04key: UTF-8 bytes; the string ends at the next byte, which cannot continue a UTF-8 character; value: 2-byte positive integer (lead byte, then a byte below 0x80)
"id": 300,69 64 C4 04key: UTF-8 bytes; the string ends at 0xC4, because the byte after it (0x04) is not a UTF-8 continuation byte, so 0xC4 starts an integer; value: 2-byte positive integer (lead byte, then a byte below 0x80)
"name": "json",6E 61 6D 65 FF 6A 73 6F 6E FFkey: UTF-8 bytes; 0xFF terminates the string as another string follows; value: UTF-8 bytes; terminated as a string key follows
"pi": 3.14,70 69 8F 40 09 1E B8 51 EB 85 1Fkey: UTF-8 bytes; value: binary64 (IEEE 754, big-endian); its marker also ends "pi"
"tags": [74 61 67 73 82key: UTF-8 bytes; value: array with 2 elements; its marker also ends "tags"