diff --git a/docs/mkdocs/docs/api/basic_json/value.md b/docs/mkdocs/docs/api/basic_json/value.md index 80f5691dc..d4b7431c9 100644 --- a/docs/mkdocs/docs/api/basic_json/value.md +++ b/docs/mkdocs/docs/api/basic_json/value.md @@ -119,6 +119,14 @@ changes to any JSON value. ## Notes +!!! warning "`null` members are not missing" + + The default value is used only if the key (or JSON Pointer) does not exist. A member that exists but is + `#!json null` is converted like any other value, so `#!cpp j.value("k", 0)` throws a + [`type_error.302`](../../home/exceptions.md#jsonexceptiontype_error302) if `"k"` is `#!json null`. See + [Access with default value](../../features/element_access/default_value.md) + for alternatives. + !!! warning "Return type" The value function is a template, and the return type of the function is determined by the type of the provided diff --git a/docs/mkdocs/docs/features/arbitrary_types.md b/docs/mkdocs/docs/features/arbitrary_types.md index 53f9cbd07..0ca165902 100644 --- a/docs/mkdocs/docs/features/arbitrary_types.md +++ b/docs/mkdocs/docs/features/arbitrary_types.md @@ -347,6 +347,13 @@ struct adl_serializer> { NLOHMANN_JSON_NAMESPACE_END ``` +!!! tip "`std::optional` needs no serializer" + + Since version 3.12.0, `std::optional` is supported out of the box when compiling with C++17 + (`std::nullopt` is converted to and from `null`). Do not write an `adl_serializer` for it; this pattern is only + needed for types such as `boost::optional` or for custom semantics. See [Conversions](conversions.md) and + [Omitting a field when serializing `std::optional`](conversions.md#omitting-a-field-when-serializing-stdoptional). + !!! note "ABI compatibility" Use [`NLOHMANN_JSON_NAMESPACE_BEGIN`](../api/macros/nlohmann_json_namespace_begin.md) and `NLOHMANN_JSON_NAMESPACE_END` diff --git a/docs/mkdocs/docs/features/element_access/default_value.md b/docs/mkdocs/docs/features/element_access/default_value.md index 481448469..be100b34b 100644 --- a/docs/mkdocs/docs/features/element_access/default_value.md +++ b/docs/mkdocs/docs/features/element_access/default_value.md @@ -30,6 +30,47 @@ you want to access and a default value in case there is no value stored with tha | `#!cpp j.value("append", false)` | `#!json true` | | `#!cpp j.value("logLevel", "verbose")` | `#!json "verbose"` | +## Nested values + +To read a value deep inside a document, pass a [JSON Pointer](../json_pointer.md) instead of a key. The default value is +returned if the value at the pointer does not exist, including the case that an intermediate key is missing. There is +no need to check each level with [`contains`](../../api/basic_json/contains.md) first. + +```cpp +json j = {{"server", {{"port", 8080}}}, {"list", {10, 20}}}; + +int port = j.value("/server/port"_json_pointer, 80); // 8080 +int timeout = j.value("/server/limits/timeout"_json_pointer, 30); // 30 (missing intermediate key) +int second = j.value("/list/1"_json_pointer, 0); // 20 (numeric tokens index arrays) +int third = j.value("/list/5"_json_pointer, 0); // 0 (index out of range) + +bool has_port = j.contains("/server/port"_json_pointer); // true +bool has_host = j.contains("/server/host/name"_json_pointer); // false +``` + +If the path is only available as a dotted string such as `#!cpp "server.port"`, do not build the pointer by +concatenating `#!cpp "/"` and the parts: keys containing `/` or `~` would be misinterpreted. Append each part as a +reference token with [`operator/=`](../../api/json_pointer/operator_slasheq.md) instead. It escapes the token for you. + +```cpp +json::json_pointer to_pointer(const std::string& dotted) +{ + json::json_pointer ptr; + std::istringstream in(dotted); + for (std::string token; std::getline(in, token, '.');) + { + ptr /= token; + } + return ptr; +} + +int port = j.value(to_pointer("server.port"), 80); // 8080 +int first = j.value(to_pointer("list.0"), 0); // 10 +``` + +The key `#!cpp "a/b"` yields the pointer `#!cpp "/a~1b"`; the dot-splitting itself is up to the caller, so keys +containing `.` need a different separator. + ## Notes !!! failure "Exceptions" @@ -37,6 +78,31 @@ you want to access and a default value in case there is no value stored with tha - With string keys, `value` can only be used with objects. For other types, a [`basic_json::type_error`](../../home/exceptions.md#jsonexceptiontype_error306) is thrown. - With JSON Pointers, `value` can be used with both objects and arrays. For other types (null, boolean, number, string), a [`basic_json::type_error`](../../home/exceptions.md#jsonexceptiontype_error306) is thrown. +!!! warning "`null` and mistyped members are not missing" + + `value` returns the default value only if the key is **absent**. If the member exists, it is converted to the type + of the default value, even if it is `#!json null`. For `#!json {"k": null}`, the call `#!cpp j.value("k", 0)` throws + a [`basic_json::type_error`](../../home/exceptions.md#jsonexceptiontype_error302), and so does a member of another + type such as a string where a number is expected. The same holds for JSON Pointers. + + To treat `#!json null` like a missing value, check for it explicitly: + + ```cpp + int n = (j.contains("k") && !j["k"].is_null()) ? j["k"].get() : 0; + ``` + + With C++17, [`get>()`](../../api/basic_json/get.md) maps `#!json null` to an empty optional. As + [`at`](../../api/basic_json/at.md) throws [`out_of_range`](../../home/exceptions.md#jsonexceptionout_of_range403) + for an absent key, use [`find`](../../api/basic_json/find.md) to cover both cases: + + ```cpp + std::optional n; // empty if "k" is absent or null + if (const auto it = j.find("k"); it != j.end()) + { + n = it->get>(); // still throws for a non-number such as "text" + } + ``` + !!! warning "Return type" The value function is a template, and the return type of the function is determined by the type of the provided @@ -63,3 +129,6 @@ you want to access and a default value in case there is no value stored with tha - [`value`](../../api/basic_json/value.md) for access with default value - documentation on [checked access](checked_access.md) +- documentation on [JSON Pointer](../json_pointer.md) +- [`contains`](../../api/basic_json/contains.md) to check whether a key or JSON Pointer exists +- [`json_pointer::operator/=`](../../api/json_pointer/operator_slasheq.md) to build a pointer token by token diff --git a/docs/mkdocs/docs/features/json_pointer.md b/docs/mkdocs/docs/features/json_pointer.md index 786832c96..1bc5eabdb 100644 --- a/docs/mkdocs/docs/features/json_pointer.md +++ b/docs/mkdocs/docs/features/json_pointer.md @@ -77,6 +77,10 @@ auto val2 = j.at(json::json_pointer("/nested/three/1")); // false auto val3 = j.value(json::json_pointer("/nested/four"), 0); // 0 ``` +To read a value with a fallback, use [`value`](../api/basic_json/value.md) with a JSON Pointer; to test for existence, +use [`contains`](../api/basic_json/contains.md). Neither needs intermediate checks, see +[Nested values](element_access/default_value.md#nested-values). + !!! note "Creating intermediate levels that don't exist" See the [`operator[]` notes](../api/basic_json/operator%5B%5D.md#return-value) for how array vs. object is @@ -126,6 +130,8 @@ auto j_original = j_flat.unflatten(); ## See also - Class [`json_pointer`](../api/json_pointer/index.md) +- Functions [`value`](../api/basic_json/value.md), [`contains`](../api/basic_json/contains.md), and + [`at`](../api/basic_json/at.md) accept JSON Pointers; see [Nested values](element_access/default_value.md#nested-values) - Function [`flatten`](../api/basic_json/flatten.md) - Function [`unflatten`](../api/basic_json/unflatten.md) - [JSON Patch](json_patch.md) - paths inside a patch are JSON Pointers diff --git a/docs/mkdocs/docs/features/merge_patch.md b/docs/mkdocs/docs/features/merge_patch.md index 46b0c5f04..c0a316e65 100644 --- a/docs/mkdocs/docs/features/merge_patch.md +++ b/docs/mkdocs/docs/features/merge_patch.md @@ -9,6 +9,13 @@ syntax that closely mimics the document being modified. Unlike [JSON Patch](json express every kind of change (e.g., it cannot reorder array elements or remove a specific array element), but it is easier to read and write for object-shaped documents. +!!! tip "Not a general deep merge" + + A merge patch is not a general deep merge: a `#!json null` value in the patch deletes the key from the target. + To merge two objects recursively (e.g., defaults and user settings), use + [`update`](../api/basic_json/update.md) with `merge_objects` set to `#!cpp true`; see + [Merging objects](modifying_values.md#merging-objects). + ??? example The following code shows how a JSON Merge Patch is applied to a JSON document. @@ -28,3 +35,4 @@ easier to read and write for object-shaped documents. - [JSON Patch and Diff](json_patch.md) - a more expressive alternative that describes a sequence of operations - [JSON Pointer](json_pointer.md) - the addressing scheme used by JSON Patch - Function [`merge_patch`](../api/basic_json/merge_patch.md) +- Function [`update`](../api/basic_json/update.md) - merge objects, optionally recursively diff --git a/docs/mkdocs/docs/features/modifying_values.md b/docs/mkdocs/docs/features/modifying_values.md index 23c0feb53..cf763a4b3 100644 --- a/docs/mkdocs/docs/features/modifying_values.md +++ b/docs/mkdocs/docs/features/modifying_values.md @@ -35,8 +35,12 @@ the insertion happened — useful for "add if absent" semantics. ## Merging objects -To merge one object into another, [`update`](../api/basic_json/update.md) copies all members from another object, -overwriting existing keys (similar to Python's `dict.update`). This is the idiomatic way to combine two objects. +To merge one object into another, [`update`](../api/basic_json/update.md) copies all members from another object +(similar to Python's `dict.update`). This is the idiomatic way to combine two objects. It has two modes: + +- By default, the merge is shallow: existing keys are overwritten, even if both values are objects. +- With `merge_objects = #!cpp true`, keys whose values are objects in both JSON values are merged recursively. + Everything else is overwritten. In particular, arrays are replaced, not concatenated. ??? example @@ -50,9 +54,22 @@ overwriting existing keys (similar to Python's `dict.update`). This is the idiom --8<-- "examples/update.output" ``` -For a recursive merge that follows [RFC 7386](https://tools.ietf.org/html/rfc7386), see -[JSON Merge Patch](merge_patch.md). To apply a sequence of well-defined edit operations, see -[JSON Patch](json_patch.md). +A common use of the recursive mode is combining defaults with user settings. Nested defaults that the user did not set +are kept: + +```cpp +json defaults = {{"log", {{"level", "info"}, {"file", "app.log"}}}, {"retries", 3}}; +json user_settings = {{"log", {{"level", "debug"}}}}; + +json config = defaults; +config.update(user_settings, true); +// {"log":{"file":"app.log","level":"debug"},"retries":3} +``` + +[JSON Merge Patch](merge_patch.md) ([RFC 7386](https://tools.ietf.org/html/rfc7386)) also merges objects recursively, +but it is a different tool: a `#!json null` in the patch means "remove this key". It is meant for applying merge patch +documents (e.g., received via HTTP PATCH). To merge configuration-like objects, use `#!cpp update(..., true)`. To apply +a sequence of well-defined edit operations, see [JSON Patch](json_patch.md). ## Removing elements @@ -72,6 +89,7 @@ a.erase(1); // [1,3,4] (erase by index) - [`push_back`](../api/basic_json/push_back.md) / [`emplace_back`](../api/basic_json/emplace_back.md) - append to an array - [`emplace`](../api/basic_json/emplace.md) - insert into an object if the key is absent -- [`update`](../api/basic_json/update.md) - merge objects +- [`update`](../api/basic_json/update.md) - merge objects (shallow, or recursive with `merge_objects`) +- [`merge_patch`](../api/basic_json/merge_patch.md) - apply an RFC 7386 merge patch - [`erase`](../api/basic_json/erase.md) / [`clear`](../api/basic_json/clear.md) - remove elements - [JSON Patch and Diff](json_patch.md) and [JSON Merge Patch](merge_patch.md) - structured modifications