From dc95cf5fa8324d4242270fa59778b7b64026f196 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Mon, 26 Oct 2026 12:07:45 +0100 Subject: [PATCH] docs: correct JSON Pointer documentation and add tests 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 4b8f6edd9..38b00b9ca 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 =