diff --git a/docs/mkdocs/docs/api/eval.md b/docs/mkdocs/docs/api/eval.md new file mode 100644 index 0000000000..bf19bbbbf9 --- /dev/null +++ b/docs/mkdocs/docs/api/eval.md @@ -0,0 +1,238 @@ +# nlohmann::eval_value, eval_array, eval_object + +```cpp +#include + +namespace nlohmann +{ + +// (1) -- access a JSON object element by key with a default fallback +template +ValueType eval_value(const BasicJsonType& j, + const typename BasicJsonType::object_t::key_type& key, + const ValueType& default_value) noexcept; + +// (2) -- access a JSON value by JSON Pointer with a default fallback +template +ValueType eval_value(const BasicJsonType& j, + const typename BasicJsonType::json_pointer& ptr, + const ValueType& default_value) noexcept; + +// (3) -- access a JSON array stored at @a key +template +const BasicJsonType& eval_array(const BasicJsonType& j, + const typename BasicJsonType::object_t::key_type& key) noexcept; + +// (4) -- access a JSON array resolved by @a ptr +template +const BasicJsonType& eval_array(const BasicJsonType& j, + const typename BasicJsonType::json_pointer& ptr) noexcept; + +// (5) -- access a JSON object stored at @a key +template +const BasicJsonType& eval_object(const BasicJsonType& j, + const typename BasicJsonType::object_t::key_type& key) noexcept; + +// (6) -- access a JSON object resolved by @a ptr +template +const BasicJsonType& eval_object(const BasicJsonType& j, + const typename BasicJsonType::json_pointer& ptr) noexcept; + +} +``` + +Null-safe, `noexcept` accessors for retrieving values from a JSON object, +where "null" refers to the JSON value [`null`](../features/types/index.md) +(or any non-matching JSON shape) rather than to a null C++ object. + +These free functions are an opt-in alternative to +[`basic_json::value`](basic_json/value.md). Unlike `value()`, they **never +throw** -- on any non-matching condition (the receiver is not a JSON +object, the key/pointer is missing, the resolved JSON value is `null`, or +has a type that is not convertible to `ValueType`) they silently fall back +to the supplied default value (overloads 1-2) or to a static empty JSON +array/object (overloads 3-6). + +This brings the developer experience of accessing untrusted server-side JSON +closer to JavaScript's optional chaining (`?.`) and nullish coalescing (`??`). + +1. Returns the value stored at @a key converted to `ValueType`. Returns + @a default_value if @a j is not a JSON object, @a key does not exist + in @a j, the value at @a key is JSON `null`, or the conversion fails. +2. As (1), but the position is identified by a + [JSON Pointer](json_pointer/index.md). Any failure to resolve @a ptr + (missing intermediate node, wrong type along the way, etc.) yields + @a default_value. +3. Returns a const reference to the JSON array stored at @a key. Returns + a reference to a static empty array if @a j is not a JSON object, + @a key does not exist, or the value at @a key is not a JSON array. +4. As (3), but the position is identified by a + [JSON Pointer](json_pointer/index.md). +5. Returns a const reference to the JSON object stored at @a key. Returns + a reference to a static empty object if @a j is not a JSON object, + @a key does not exist, or the value at @a key is not a JSON object. +6. As (5), but the position is identified by a + [JSON Pointer](json_pointer/index.md). + +## Template parameters + +`BasicJsonType` +: a specialization of [`basic_json`](basic_json/index.md). + +`ValueType` +: a type compatible with JSON values. For overloads (1) and (2), it is + deduced from @a default_value, so the common case never requires + explicit template arguments. + +## Parameters + +`j` (in) +: the JSON value to query. + +`key` (in) +: the object key to look up (overloads (1), (3), (5)). + +`ptr` (in) +: a [JSON Pointer](json_pointer/index.md) identifying the position to + look up (overloads (2), (4), (6)). + +`default_value` (in) +: the value to return when no matching value can be retrieved + (overloads (1) and (2)). + +## Return value + +1. The value stored at @a key converted to `ValueType`, or @a default_value + on any non-matching condition. +2. The JSON value resolved by @a ptr converted to `ValueType`, or + @a default_value on any non-matching condition. +3. A const reference to the JSON array stored at @a key, or a const + reference to a static empty array on any non-matching condition. +4. A const reference to the JSON array resolved by @a ptr, or a const + reference to a static empty array on any non-matching condition. +5. A const reference to the JSON object stored at @a key, or a const + reference to a static empty object on any non-matching condition. +6. A const reference to the JSON object resolved by @a ptr, or a const + reference to a static empty object on any non-matching condition. + +## Exception safety + +All overloads are declared `noexcept` and never throw. + +## Complexity + +1. Logarithmic in the size of @a j (one object lookup). +2. Linear in the length of @a ptr. +3. Logarithmic in the size of @a j. +4. Linear in the length of @a ptr. +5. Logarithmic in the size of @a j. +6. Linear in the length of @a ptr. + +## Notes + +!!! note "Comparison with `value()`" + + | Condition | `j.value(...)` | `eval_value(j, ...)` | + | -------------------------------------------- | ---------------------------- | -------------------- | + | `j` is object, key exists, correct type | returns value | returns value | + | `j` is object, key missing | returns default | returns default | + | `j` is JSON `null` | **throws `type_error`** | returns default | + | `j` is array, string, number, bool, ... | **throws `type_error`** | returns default | + | resolved value is JSON `null` | returns null-converted value | returns default | + +!!! note "Implementation strategy" + + These helpers rely only on the public API of + [`basic_json`](basic_json/index.md): `is_object`, `is_array`, + `is_null`, `find`, `end`, `get`, `contains(json_pointer)`, and + `at(json_pointer)`. They are intentionally provided as non-member + functions in an opt-in header (``). They are not + pulled in by `` and are not bundled into the + amalgamated `single_include/nlohmann/json.hpp`. + + The empty fallback array/object returned by reference is a + process-lifetime singleton constructed once into properly-aligned + uninitialized storage via placement-new. Its destructor is + intentionally never invoked at process exit, which avoids both + Clang's `-Wexit-time-destructors` and any + static-destruction-order concerns. + +!!! warning "Limitation under `JSON_NOEXCEPTION`" + + The `noexcept` guarantee is best-effort under + [`JSON_NOEXCEPTION`](macros/json_noexception.md): + + - `eval_array` and `eval_object` (both key and JSON Pointer overloads) + and the JSON Pointer overload of `eval_value` remain fully + noexcept-correct, because they only rely on `is_*` predicates, + `find`, `contains`, and `at` paths that are guarded by a successful + `contains` check. + - `eval_value(j, key, default)` calls `it->get()` for type + conversion. Under `JSON_NOEXCEPTION`, a conversion failure inside + `from_json` calls `std::abort()` instead of throwing, so passing a + receiver where the value at @a key is not convertible to `ValueType` + may abort the process. Use the JSON Pointer overload, or perform an + explicit `is_*` check at the call site, when running with exceptions + disabled. + +## Examples + +??? example "Example: safe access on a possibly-null payload" + + The example below demonstrates safe defensive access to a JSON value + received from an untrusted source. + + ```cpp + #include + #include + + using nlohmann::json; + + // a payload received from a remote source -- could be JSON null, + // partial, of the wrong type, or fully populated + json received = nullptr; + + int a = nlohmann::eval_value(received, "a", 0); + auto d = nlohmann::eval_value(received, + json::json_pointer("/c/d"), + std::string{}); + + // safe range-based for: returns an empty array when "items" is missing + // or has the wrong shape + for (const auto& item : nlohmann::eval_array(received, "items")) + { + // process each item + (void) item; + } + + // safe iteration over a nested object + for (const auto& kv : nlohmann::eval_object(received, "metadata").items()) + { + // process each key/value pair + (void) kv; + } + ``` + +??? example "Example: ADL allows omitting the namespace qualifier" + + Because the helpers live in `namespace nlohmann`, they are found by + argument-dependent lookup, so the explicit qualifier is optional in + user code: + + ```cpp + const json j = {{"a", 7}}; + auto a = eval_value(j, "a", 0); // ADL finds nlohmann::eval_value + ``` + +## See also + +- [`basic_json::value`](basic_json/value.md) -- exception-throwing + counterpart with different semantics on non-object receivers. +- [JSON Pointer](json_pointer/index.md) -- the address syntax accepted + by the pointer overloads. +- Discussion [#5129](https://github.com/nlohmann/json/discussions/5129) -- + motivation and design rationale. + +## Version history + +- Added in version 3.12.1. diff --git a/include/nlohmann/eval.hpp b/include/nlohmann/eval.hpp new file mode 100644 index 0000000000..e05d16f245 --- /dev/null +++ b/include/nlohmann/eval.hpp @@ -0,0 +1,330 @@ +// __ _____ _____ _____ +// __| | __| | | | 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 // array, used for aligned storage of the empty singletons +#include // placement new + +#include +#include + +// ----------------------------------------------------------------------------- +// Local exception-handling shim. +// +// The library-wide JSON_TRY / JSON_INTERNAL_CATCH macros are intentionally +// undef'd at the end of via macro_unscope.hpp, so they are +// not visible to consumers that include this header after json.hpp. Define a +// header-private equivalent that respects JSON_NOEXCEPTION the same way. +// ----------------------------------------------------------------------------- +#if (defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND)) && !defined(JSON_NOEXCEPTION) + #define NLOHMANN_EVAL_TRY try + #define NLOHMANN_EVAL_CATCH_ALL catch (...) +#else + #define NLOHMANN_EVAL_TRY if (true) + #define NLOHMANN_EVAL_CATCH_ALL if (false) +#endif + +NLOHMANN_JSON_NAMESPACE_BEGIN + +// ============================================================================= +// Null-safe, noexcept accessors (see discussion #5129) +// +// These free functions provide null-safe, noexcept access to JSON values. +// They never throw -- on any non-matching condition (the receiver is not an +// object, the key/pointer is missing, the resolved value is null, or it has +// the wrong type) they silently fall back to the supplied default value (for +// `eval_value`) or to a static empty array/object (for `eval_array` / +// `eval_object`). +// +// The non-member form is preferred over additional members on `basic_json`: +// * it relies only on the public API (`is_object`, `is_array`, `is_null`, +// `find`, `end`, `get`, `at(json_pointer)`); +// * it does not enlarge the (already large) `basic_json` interface; +// * it is fully resolvable via ADL (`eval_value(j, "a", 0)`); +// * it lives in an opt-in header so users who do not need it pay nothing. +// +// Example: +// auto j = from_server(); // may be null +// int a = nlohmann::eval_value(j, "a", 0); // safe +// auto d = nlohmann::eval_value(j, +// "/c/d"_json_pointer, +// std::string{}); // safe +// for (const auto& item : nlohmann::eval_array(j, "items")) { /* ... */ } +// ============================================================================= + +namespace detail +{ + +// Singleton holding an immortalized empty JSON value of a given value_t. +// +// Implementation note: a plain `static const BasicJsonType instance(Kind);` +// would trigger Clang's -Wexit-time-destructors (which the project treats as +// an error). Instead we construct the value once into properly-aligned +// uninitialized storage via placement-new and return a reference to it. The +// destructor is intentionally never invoked at process exit, which is safe +// for an empty `array`/`object` constant: it owns no resources beyond the +// internal allocator state, and skipping its destructor avoids any +// static-destruction-order concerns. +// +// NOLINTBEGIN(bugprone-exception-escape) -- BasicJsonType's default-allocator +// constructor is non-throwing in practice for the array/object value_t we +// instantiate this with; the noexcept here is the contract callers rely on. +template +const BasicJsonType& empty_json_singleton() noexcept +{ + // Aligned, fixed-size byte storage holding the immortalized + // BasicJsonType. Using std::array (rather than a C-style array) + // avoids `cppcoreguidelines-avoid-c-arrays` warnings and the matching + // flawfinder `buffer/char` heuristic. + using storage_t = std::array; + alignas(BasicJsonType) static storage_t storage{}; + + // Construct-once on first call. The pointer's type is a raw pointer + // (trivially destructible), so this static local also does not require + // an exit-time destructor. + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) -- intentional process-lifetime singleton + static const BasicJsonType* const instance = + ::new (static_cast(storage.data())) BasicJsonType(Kind); + + return *instance; +} +// NOLINTEND(bugprone-exception-escape) + +} // namespace detail + +// ----------------------------------------------------------------------------- +// eval_value -- noexcept value access with default +// ----------------------------------------------------------------------------- + +/// @brief access a value by key with a default fallback (noexcept) +/// +/// Returns @a default_value if any of the following holds: +/// - @a j is not an object (null, array, string, number, boolean, ...); +/// - @a j is an object but @a key is missing; +/// - the value at @a key is null; +/// - the value at @a key cannot be converted to @c ValueType. +/// +/// Never throws. +template +ValueType eval_value(const BasicJsonType& j, + const typename BasicJsonType::object_t::key_type& key, + const ValueType& default_value) noexcept +{ + if (!j.is_object()) + { + return default_value; + } + + NLOHMANN_EVAL_TRY + { + const auto it = j.find(key); + if (it != j.end() && !it->is_null()) + { + return it->template get(); + } + } + NLOHMANN_EVAL_CATCH_ALL {} + + return default_value; +} + +/// @brief access a value by JSON Pointer with a default fallback (noexcept) +/// +/// Returns @a default_value if any of the following holds: +/// - @a j is not an object; +/// - any segment of @a ptr cannot be resolved (missing, wrong type, ...); +/// - the resolved value is null; +/// - the resolved value cannot be converted to @c ValueType. +/// +/// Never throws. +template +ValueType eval_value(const BasicJsonType& j, + const typename BasicJsonType::json_pointer& ptr, + const ValueType& default_value) noexcept +{ + if (!j.is_object()) + { + return default_value; + } + + NLOHMANN_EVAL_TRY + { + if (!j.contains(ptr)) + { + return default_value; + } + const auto& resolved = j.at(ptr); + if (resolved.is_null()) + { + return default_value; + } + return resolved.template get(); + } + NLOHMANN_EVAL_CATCH_ALL + { + return default_value; + } +} + +// ----------------------------------------------------------------------------- +// eval_array -- noexcept array access (returns a const reference) +// ----------------------------------------------------------------------------- + +/// @brief access an array by key (noexcept) +/// +/// Returns a const reference to the array stored at @a key. Returns a const +/// reference to a static empty array if any of the following holds: +/// - @a j is not an object; +/// - @a key is missing; +/// - the value at @a key is not an array. +/// +/// Never throws. +template +const BasicJsonType& eval_array( + const BasicJsonType& j, + const typename BasicJsonType::object_t::key_type& key) noexcept +{ + const auto& empty = detail::empty_json_singleton(); + + if (!j.is_object()) + { + return empty; + } + + NLOHMANN_EVAL_TRY + { + const auto it = j.find(key); + if (it != j.end() && it->is_array()) + { + return *it; + } + } + NLOHMANN_EVAL_CATCH_ALL {} + + return empty; +} + +/// @brief access an array by JSON Pointer (noexcept) +/// +/// Returns a const reference to the array resolved by @a ptr. Returns a const +/// reference to a static empty array if any of the following holds: +/// - @a j is not an object; +/// - @a ptr cannot be resolved; +/// - the resolved value is not an array. +/// +/// Never throws. +template +const BasicJsonType& eval_array( + const BasicJsonType& j, + const typename BasicJsonType::json_pointer& ptr) noexcept +{ + const auto& empty = detail::empty_json_singleton(); + + if (!j.is_object()) + { + return empty; + } + + NLOHMANN_EVAL_TRY + { + if (!j.contains(ptr)) + { + return empty; + } + const auto& resolved = j.at(ptr); + if (resolved.is_array()) + { + return resolved; + } + } + NLOHMANN_EVAL_CATCH_ALL {} + + return empty; +} + +// ----------------------------------------------------------------------------- +// eval_object -- noexcept object access (returns a const reference) +// ----------------------------------------------------------------------------- + +/// @brief access an object by key (noexcept) +/// +/// Returns a const reference to the object stored at @a key. Returns a const +/// reference to a static empty object if any of the following holds: +/// - @a j is not an object; +/// - @a key is missing; +/// - the value at @a key is not an object. +/// +/// Never throws. +template +const BasicJsonType& eval_object( + const BasicJsonType& j, + const typename BasicJsonType::object_t::key_type& key) noexcept +{ + const auto& empty = detail::empty_json_singleton(); + + if (!j.is_object()) + { + return empty; + } + + NLOHMANN_EVAL_TRY + { + const auto it = j.find(key); + if (it != j.end() && it->is_object()) + { + return *it; + } + } + NLOHMANN_EVAL_CATCH_ALL {} + + return empty; +} + +/// @brief access an object by JSON Pointer (noexcept) +/// +/// Returns a const reference to the object resolved by @a ptr. Returns a const +/// reference to a static empty object if any of the following holds: +/// - @a j is not an object; +/// - @a ptr cannot be resolved; +/// - the resolved value is not an object. +/// +/// Never throws. +template +const BasicJsonType& eval_object( + const BasicJsonType& j, + const typename BasicJsonType::json_pointer& ptr) noexcept +{ + const auto& empty = detail::empty_json_singleton(); + + if (!j.is_object()) + { + return empty; + } + + NLOHMANN_EVAL_TRY + { + if (!j.contains(ptr)) + { + return empty; + } + const auto& resolved = j.at(ptr); + if (resolved.is_object()) + { + return resolved; + } + } + NLOHMANN_EVAL_CATCH_ALL {} + + return empty; +} + +NLOHMANN_JSON_NAMESPACE_END + +#undef NLOHMANN_EVAL_TRY +#undef NLOHMANN_EVAL_CATCH_ALL diff --git a/tests/src/unit-eval.cpp b/tests/src/unit-eval.cpp new file mode 100644 index 0000000000..4cc826f91a --- /dev/null +++ b/tests/src/unit-eval.cpp @@ -0,0 +1,460 @@ +// __ _____ _____ _____ +// __| | __| | | | 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 + +// The eval_* helpers live in an opt-in header that is intentionally NOT +// part of the amalgamated single-include `single_include/nlohmann/json.hpp` +// (in line with the design discussion in #5129). Skip this test file when +// the test suite is built against the single-header amalgamation. +// +// Also skip when JSON_NOEXCEPTION is defined: the helpers' pointer +// overloads remain correct under that mode, but `eval_value(j, key, T{})` +// must call `it->template get()` whose internal `JSON_THROW` becomes +// `std::abort()` -- there is no public, non-throwing conversion that the +// helpers could fall back to. The helpers are therefore not exercised by +// the no-exceptions matrix; the same approach is taken by other unit-* +// files that exercise public APIs which can throw type_error. +#if defined(JSON_TEST_USING_MULTIPLE_HEADERS) && JSON_TEST_USING_MULTIPLE_HEADERS \ + && !defined(JSON_NOEXCEPTION) + +#include +using nlohmann::json; + +#include + +// Tests for null-safe, noexcept accessors: +// eval_value(j, key | ptr, default) +// eval_array(j, key | ptr) +// eval_object(j, key | ptr) +// +// See https://github.com/nlohmann/json/discussions/5129. +// +// NOTE on style: when calling reference-returning helpers (eval_array, +// eval_object), the key (std::string) and json_pointer arguments are kept in +// named local variables on purpose -- this matches the recommended user +// style and avoids GCC's -Wdangling-reference warning that triggers when a +// function returning a reference takes a parameter bound to a temporary +// (the warning is a false positive here, but worth modelling correctly). + +TEST_CASE("eval_value with key") +{ + SECTION("happy path: object with matching key and type") + { + const json j = {{"a", 42}, {"s", "hello"}}; + CHECK(nlohmann::eval_value(j, "a", 0) == 42); + CHECK(nlohmann::eval_value(j, "s", std::string{"x"}) == "hello"); + } + + SECTION("missing key returns default") + { + const json j = {{"a", 1}}; + CHECK(nlohmann::eval_value(j, "missing", 7) == 7); + CHECK(nlohmann::eval_value(j, "missing", std::string{"def"}) == "def"); + } + + SECTION("null value at key returns default") + { + const json j = {{"a", nullptr}}; + CHECK(nlohmann::eval_value(j, "a", 99) == 99); + } + + SECTION("wrong type at key returns default") + { + const json j = {{"a", "not a number"}}; + CHECK(nlohmann::eval_value(j, "a", 5) == 5); + } + + SECTION("non-object receiver returns default") + { + const json null_j = nullptr; + const json arr_j = json::array({1, 2, 3}); + const json str_j = "hello"; + const json num_j = 42; + const json bool_j = true; + + CHECK(nlohmann::eval_value(null_j, "a", 1) == 1); + CHECK(nlohmann::eval_value(arr_j, "a", 1) == 1); + CHECK(nlohmann::eval_value(str_j, "a", 1) == 1); + CHECK(nlohmann::eval_value(num_j, "a", 1) == 1); + CHECK(nlohmann::eval_value(bool_j, "a", 1) == 1); + } + + SECTION("noexcept") + { + const json j = {{"a", 1}}; + const json::object_t::key_type key = "a"; + const int def = 0; + // Pre-construct the arguments so noexcept(...) measures eval_value + // itself, not the (possibly throwing) construction of std::string + // / int from the literal arguments. + CHECK(noexcept(nlohmann::eval_value(j, key, def))); + } +} + +TEST_CASE("eval_value with JSON Pointer") +{ + SECTION("happy path: nested object") + { + const json j = {{"c", {{"d", "deep"}}}}; + const auto ptr = json::json_pointer("/c/d"); + CHECK(nlohmann::eval_value(j, ptr, std::string{}) == "deep"); + } + + SECTION("unresolvable pointer returns default") + { + const json j = {{"c", {{"d", "deep"}}}}; + const auto p1 = json::json_pointer("/c/missing"); + const auto p2 = json::json_pointer("/x/y/z"); + CHECK(nlohmann::eval_value(j, p1, std::string{"def"}) == "def"); + CHECK(nlohmann::eval_value(j, p2, std::string{"def"}) == "def"); + } + + SECTION("null intermediate or null resolved value returns default") + { + const json j1 = {{"c", nullptr}}; + const auto pcd = json::json_pointer("/c/d"); + // /c is null -> walking to /c/d is unresolvable -> default + CHECK(nlohmann::eval_value(j1, pcd, std::string{"def"}) == "def"); + + const json j2 = {{"c", {{"d", nullptr}}}}; + CHECK(nlohmann::eval_value(j2, pcd, std::string{"def"}) == "def"); + } + + SECTION("wrong resolved type returns default") + { + const json j = {{"a", "not a number"}}; + const auto pa = json::json_pointer("/a"); + CHECK(nlohmann::eval_value(j, pa, 5) == 5); + } + + SECTION("non-object receiver returns default") + { + const json null_j = nullptr; + const json arr_j = json::array({1, 2, 3}); + const auto pa = json::json_pointer("/a"); + const auto p0 = json::json_pointer("/0"); + + CHECK(nlohmann::eval_value(null_j, pa, 7) == 7); + CHECK(nlohmann::eval_value(arr_j, p0, 7) == 7); + } + + SECTION("noexcept") + { + const json j = {{"a", 1}}; + const auto ptr = json::json_pointer("/a"); + CHECK(noexcept(nlohmann::eval_value(j, ptr, 0))); + } +} + +TEST_CASE("eval_array with key") +{ + // Keep the key alive in a named variable: eval_array returns a + // reference, so binding it to `const auto&` while passing a temporary + // std::string for the key would otherwise trip GCC's + // -Wdangling-reference (false positive here, but worth modelling). + const json::object_t::key_type k_items = "items"; + const json::object_t::key_type k_missing = "missing"; + const json::object_t::key_type k_x = "x"; + const json::object_t::key_type k_y = "y"; + + SECTION("happy path: returns const reference to array") + { + const json j = {{"items", {1, 2, 3}}}; + const auto& arr = nlohmann::eval_array(j, k_items); + REQUIRE(arr.is_array()); + CHECK(arr.size() == 3); + CHECK(arr[0] == 1); + CHECK(arr[2] == 3); + } + + SECTION("range-based for loop is safe") + { + const json j = {{"items", {10, 20, 30}}}; + int sum = 0; + const auto& arr = nlohmann::eval_array(j, k_items); + for (const auto& item : arr) + { + sum += item.template get(); + } + CHECK(sum == 60); + } + + SECTION("missing key returns empty array") + { + const json j = {{"a", 1}}; + const auto& arr = nlohmann::eval_array(j, k_missing); + REQUIRE(arr.is_array()); + CHECK(arr.empty()); + } + + SECTION("wrong type at key returns empty array") + { + const json j = {{"items", "not an array"}}; + const auto& arr = nlohmann::eval_array(j, k_items); + REQUIRE(arr.is_array()); + CHECK(arr.empty()); + } + + SECTION("non-object receiver returns empty array") + { + const json null_j = nullptr; + const json arr_j = json::array({1, 2, 3}); + const json num_j = 42; + + const auto& a1 = nlohmann::eval_array(null_j, k_items); + const auto& a2 = nlohmann::eval_array(arr_j, k_items); + const auto& a3 = nlohmann::eval_array(num_j, k_items); + + CHECK(a1.is_array()); + CHECK(a1.empty()); + CHECK(a2.is_array()); + CHECK(a2.empty()); + CHECK(a3.is_array()); + CHECK(a3.empty()); + } + + SECTION("static empty array reference is stable") + { + const json j = {{"a", 1}}; + const auto& a = nlohmann::eval_array(j, k_x); + const auto& b = nlohmann::eval_array(j, k_y); + // Both should reference the same static singleton. + CHECK(&a == &b); + } + + SECTION("noexcept") + { + const json j = {{"items", json::array()}}; + CHECK(noexcept(nlohmann::eval_array(j, k_items))); + } +} + +TEST_CASE("eval_array with JSON Pointer") +{ + const auto p_entries = json::json_pointer("/response/data/entries"); + const auto p_xyz = json::json_pointer("/x/y/z"); + const auto p_x = json::json_pointer("/x"); + const auto p_a = json::json_pointer("/a"); + + SECTION("happy path: nested array") + { + const json j = {{"response", {{"data", {{"entries", {1, 2, 3}}}}}}}; + const auto& arr = nlohmann::eval_array(j, p_entries); + REQUIRE(arr.is_array()); + CHECK(arr.size() == 3); + } + + SECTION("unresolvable pointer returns empty array") + { + const json j = {{"a", 1}}; + const auto& arr = nlohmann::eval_array(j, p_xyz); + REQUIRE(arr.is_array()); + CHECK(arr.empty()); + } + + SECTION("wrong resolved type returns empty array") + { + const json j = {{"x", "not an array"}}; + const auto& arr = nlohmann::eval_array(j, p_x); + REQUIRE(arr.is_array()); + CHECK(arr.empty()); + } + + SECTION("non-object receiver returns empty array") + { + const json null_j = nullptr; + const auto& arr = nlohmann::eval_array(null_j, p_a); + CHECK(arr.is_array()); + CHECK(arr.empty()); + } + + SECTION("noexcept") + { + const json j = {{"a", json::array()}}; + CHECK(noexcept(nlohmann::eval_array(j, p_a))); + } +} + +TEST_CASE("eval_object with key") +{ + const json::object_t::key_type k_meta = "meta"; + const json::object_t::key_type k_missing = "missing"; + const json::object_t::key_type k_x = "x"; + const json::object_t::key_type k_y = "y"; + + SECTION("happy path: returns const reference to object") + { + const json j = {{"meta", {{"k", 1}, {"v", 2}}}}; + const auto& obj = nlohmann::eval_object(j, k_meta); + REQUIRE(obj.is_object()); + CHECK(obj.size() == 2); + CHECK(obj.at("k") == 1); + } + + SECTION("range-based for over items() is safe") + { + const json j = {{"meta", {{"a", 1}, {"b", 2}}}}; + int count = 0; + const auto& obj = nlohmann::eval_object(j, k_meta); + for (const auto& kv : obj.items()) + { + (void)kv; + ++count; + } + CHECK(count == 2); + } + + SECTION("missing key returns empty object") + { + const json j = {{"a", 1}}; + const auto& obj = nlohmann::eval_object(j, k_missing); + REQUIRE(obj.is_object()); + CHECK(obj.empty()); + } + + SECTION("wrong type at key returns empty object") + { + const json j = {{"meta", "not an object"}}; + const auto& obj = nlohmann::eval_object(j, k_meta); + REQUIRE(obj.is_object()); + CHECK(obj.empty()); + } + + SECTION("non-object receiver returns empty object") + { + const json null_j = nullptr; + const json arr_j = json::array({1, 2, 3}); + + const auto& o1 = nlohmann::eval_object(null_j, k_meta); + const auto& o2 = nlohmann::eval_object(arr_j, k_meta); + + CHECK(o1.is_object()); + CHECK(o1.empty()); + CHECK(o2.is_object()); + CHECK(o2.empty()); + } + + SECTION("static empty object reference is stable") + { + const json j = {{"a", 1}}; + const auto& a = nlohmann::eval_object(j, k_x); + const auto& b = nlohmann::eval_object(j, k_y); + CHECK(&a == &b); + } + + SECTION("array vs object empty singletons differ") + { + const json j = {{"a", 1}}; + const auto& arr = nlohmann::eval_array(j, k_x); + const auto& obj = nlohmann::eval_object(j, k_x); + CHECK(arr.is_array()); + CHECK(obj.is_object()); + } + + SECTION("noexcept") + { + const json j = {{"meta", json::object()}}; + CHECK(noexcept(nlohmann::eval_object(j, k_meta))); + } +} + +TEST_CASE("eval_object with JSON Pointer") +{ + const auto p_cd = json::json_pointer("/c/d"); + const auto p_xyz = json::json_pointer("/x/y/z"); + const auto p_x = json::json_pointer("/x"); + const auto p_a = json::json_pointer("/a"); + + SECTION("happy path: nested object") + { + const json j = {{"c", {{"d", {{"e", 1}}}}}}; + const auto& obj = nlohmann::eval_object(j, p_cd); + REQUIRE(obj.is_object()); + CHECK(obj.at("e") == 1); + } + + SECTION("unresolvable pointer returns empty object") + { + const json j = {{"a", 1}}; + const auto& obj = nlohmann::eval_object(j, p_xyz); + REQUIRE(obj.is_object()); + CHECK(obj.empty()); + } + + SECTION("wrong resolved type returns empty object") + { + const json j = {{"x", json::array({1, 2})}}; + const auto& obj = nlohmann::eval_object(j, p_x); + REQUIRE(obj.is_object()); + CHECK(obj.empty()); + } + + SECTION("non-object receiver returns empty object") + { + const json null_j = nullptr; + const auto& obj = nlohmann::eval_object(null_j, p_a); + CHECK(obj.is_object()); + CHECK(obj.empty()); + } + + SECTION("noexcept") + { + const json j = {{"a", json::object()}}; + CHECK(noexcept(nlohmann::eval_object(j, p_a))); + } +} + +TEST_CASE("eval functions end-to-end scenario from discussion 5129") +{ + SECTION("safe access on a possibly-null payload") + { + // Simulate `auto received = from_server();` with a null payload. + const json received = nullptr; + + const auto p_cd = json::json_pointer("/c/d"); + const int a = nlohmann::eval_value(received, "a", 0); + const auto d = nlohmann::eval_value(received, p_cd, std::string{}); + + CHECK(a == 0); + CHECK(d.empty()); + + // Range-based for is still safe on a null receiver. + const json::object_t::key_type k_items = "items"; + const auto& items = nlohmann::eval_array(received, k_items); + int count = 0; + for (const auto& item : items) + { + (void)item; + ++count; + } + CHECK(count == 0); + } + + SECTION("safe access on a partial payload") + { + const json received = {{"a", 5}}; // no "c", no "items" + const auto p_cd = json::json_pointer("/c/d"); + const json::object_t::key_type k_items = "items"; + + CHECK(nlohmann::eval_value(received, "a", 0) == 5); + CHECK(nlohmann::eval_value(received, p_cd, std::string{"fallback"}) == "fallback"); + CHECK(nlohmann::eval_array(received, k_items).empty()); + } + + SECTION("ADL works without explicit namespace qualification") + { + const json j = {{"a", 7}}; + // ADL: argument-dependent lookup finds nlohmann::eval_value via `j`. + CHECK(eval_value(j, "a", 0) == 7); + } +} + +#endif // JSON_TEST_USING_MULTIPLE_HEADERS && !JSON_NOEXCEPTION