From 6dc30e3864cc73df69bbcd801fecf160cdb26c39 Mon Sep 17 00:00:00 2001 From: gitpaladin Date: Tue, 26 May 2026 14:15:06 +0800 Subject: [PATCH 1/3] Add null-safe noexcept accessors eval_value/eval_array/eval_object Introduce a small, opt-in header providing three free function templates that complement basic_json::value() with semantics designed for untrusted JSON payloads: - eval_value(j, key|ptr, default) -- noexcept, returns default on any non-matching condition (non-object receiver, missing key/path, null resolved value, wrong type, conversion failure). - eval_array(j, key|ptr) -- noexcept, returns const ref to a static empty array on any non-matching condition. - eval_object(j, key|ptr) -- noexcept, returns const ref to a static empty object on any non-matching condition. Design follows the discussion on #5129: * Implemented as non-member functions in namespace nlohmann so that ADL works without explicit qualification (eval_value(j, ...) just works). * Lives in a dedicated opt-in header so basic_json's already large public API is not extended (per maintainer preference in the discussion). * Uses only the public API of basic_json (is_object, is_array, find, end, get, contains(json_pointer), at(json_pointer)). The pointer overloads guard with j.contains(ptr) before j.at(ptr), which is correct under JSON_NOEXCEPTION too (where at() would otherwise abort instead of throwing). * Static empty array/object fallbacks are returned by const reference via Meyers' singletons, so they incur no per-call allocation and are thread-safe. * Header-private NLOHMANN_EVAL_TRY / NLOHMANN_EVAL_CATCH_ALL macros mirror the JSON_TRY / JSON_INTERNAL_CATCH semantics (the library's own macros are intentionally undef'd at the end of json.hpp via macro_unscope.hpp and are therefore unavailable to consumers). Tests (tests/src/unit-eval.cpp): 7 test cases, 79 assertions, covering happy paths, missing keys/pointers, null resolved values, wrong resolved types, non-object receivers, ADL invocation, stable singleton identity, range-based for safety, and noexcept(...) probes. Documentation: docs/mkdocs/docs/api/eval.md describes the API, semantics, comparison with value(), and design notes. Signed-off-by: gitpaladin --- docs/mkdocs/docs/api/eval.md | 144 ++++++++++++ include/nlohmann/eval.hpp | 297 +++++++++++++++++++++++ tests/src/unit-eval.cpp | 442 +++++++++++++++++++++++++++++++++++ 3 files changed, 883 insertions(+) create mode 100644 docs/mkdocs/docs/api/eval.md create mode 100644 include/nlohmann/eval.hpp create mode 100644 tests/src/unit-eval.cpp diff --git a/docs/mkdocs/docs/api/eval.md b/docs/mkdocs/docs/api/eval.md new file mode 100644 index 0000000000..550d36c053 --- /dev/null +++ b/docs/mkdocs/docs/api/eval.md @@ -0,0 +1,144 @@ +# nlohmann::eval_value, eval_array, eval_object + +```cpp +#include +``` + +Null-safe, `noexcept` accessors for retrieving values from a JSON 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 an object, the +key/pointer is missing, the resolved value is null, or 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`). + +This brings the developer experience of accessing untrusted server-side JSON +closer to JavaScript's optional chaining (`?.`) and nullish coalescing (`??`). + +## API + +```cpp +// (1) -- access by key +template +ValueType eval_value(const BasicJsonType& j, + const typename BasicJsonType::object_t::key_type& key, + const ValueType& default_value) noexcept; + +// (2) -- access by JSON Pointer +template +ValueType eval_value(const BasicJsonType& j, + const typename BasicJsonType::json_pointer& ptr, + const ValueType& default_value) noexcept; + +// (3) -- array access by key +template +const BasicJsonType& eval_array( + const BasicJsonType& j, + const typename BasicJsonType::object_t::key_type& key) noexcept; + +// (4) -- array access by JSON Pointer +template +const BasicJsonType& eval_array( + const BasicJsonType& j, + const typename BasicJsonType::json_pointer& ptr) noexcept; + +// (5) -- object access by key +template +const BasicJsonType& eval_object( + const BasicJsonType& j, + const typename BasicJsonType::object_t::key_type& key) noexcept; + +// (6) -- object access by JSON Pointer +template +const BasicJsonType& eval_object( + const BasicJsonType& j, + const typename BasicJsonType::json_pointer& ptr) noexcept; +``` + +## Semantics + +| Function | On non-object receiver | On missing key/path | On null resolved value | On wrong resolved type | +| ----------------------------- | ---------------------- | ------------------- | ---------------------- | ---------------------- | +| `eval_value(j, key, default)` | returns `default` | returns `default` | returns `default` | returns `default` | +| `eval_value(j, ptr, default)` | returns `default` | returns `default` | returns `default` | returns `default` | +| `eval_array(j, key)` | returns empty `[]` | returns empty `[]` | returns empty `[]` | returns empty `[]` | +| `eval_array(j, ptr)` | returns empty `[]` | returns empty `[]` | returns empty `[]` | returns empty `[]` | +| `eval_object(j, key)` | returns empty `{}` | returns empty `{}` | returns empty `{}` | returns empty `{}` | +| `eval_object(j, ptr)` | returns empty `{}` | returns empty `{}` | returns empty `{}` | returns empty `{}` | + +All overloads are `noexcept`. They never throw regardless of the receiver's +type or the structure of the JSON value. + +## 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 `null` | **throws `type_error`** | returns default | +| `j` is array, string, number, bool, ... | **throws `type_error`** | returns default | +| Resolved value is `null` | returns null-converted value | returns default | + +## Examples + +### Safe access on a possibly-null payload + +```cpp +#include +#include + +using nlohmann::json; + +auto received = from_server(); // might be null, partial, or wrong type + +int a = nlohmann::eval_value(received, "a", 0); +auto d = nlohmann::eval_value(received, + json::json_pointer("/c/d"), + std::string{}); + +for (const auto& item : nlohmann::eval_array(received, "items")) +{ + // safe, no need to check is_object() / contains() / is_array() +} + +for (const auto& [k, v] : nlohmann::eval_object(received, "metadata").items()) +{ + // safe, no exceptions +} +``` + +### ADL + +Because `eval_*` lives in `namespace nlohmann`, it is found by +argument-dependent lookup -- you can omit the namespace qualifier: + +```cpp +const json j = {{"a", 7}}; +auto a = eval_value(j, "a", 0); // ADL finds nlohmann::eval_value +``` + +## Design notes + +- These helpers rely only on the **public** API of `basic_json` + (`is_object`, `find`, `end`, `get`, `is_null`, `is_array`, `is_object`) + and `json_pointer::get_checked`. +- They are intentionally provided as **non-member** functions in an + **opt-in** header (``). They are not pulled in by + ``. +- The empty fallback array/object returned by reference is a + `static const` Meyers' singleton, so it is allocated once and is safe + for concurrent reads (thread-safe since C++11). +- `ValueType` for `eval_value` is **deduced** from `default_value`, so + the common case never requires explicit template arguments. + +## See also + +- [`basic_json::value`](basic_json/value.md) -- exception-throwing + counterpart with different semantics on non-object receivers. +- 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..1a7765b48b --- /dev/null +++ b/include/nlohmann/eval.hpp @@ -0,0 +1,297 @@ +// __ _____ _____ _____ +// __| | __| | | | 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 +#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 +{ + +// Meyers' singleton holding a static empty JSON value of a given value_t. +// Returning by const reference avoids per-call allocation and is thread-safe +// since C++11. +template +const BasicJsonType& empty_json_singleton() noexcept +{ + static const BasicJsonType instance(Kind); + return instance; +} + +} // 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; + } + + const auto it = j.find(key); + if (it != j.end() && !it->is_null()) + { + NLOHMANN_EVAL_TRY + { + return it->template get(); + } + NLOHMANN_EVAL_CATCH_ALL + { + return default_value; + } + } + + 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; + } + + const auto it = j.find(key); + if (it != j.end() && it->is_array()) + { + return *it; + } + + 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; + } + + const auto it = j.find(key); + if (it != j.end() && it->is_object()) + { + return *it; + } + + 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..1da43a1b56 --- /dev/null +++ b/tests/src/unit-eval.cpp @@ -0,0 +1,442 @@ +// __ _____ _____ _____ +// __| | __| | | | 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 +#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); + } +} From b88fd861bdfa20f661a37e3278efcf2191c9d568 Mon Sep 17 00:00:00 2001 From: gitpaladin Date: Wed, 27 May 2026 09:07:29 +0800 Subject: [PATCH 2/3] Fix CI failures: exit-time-destructors, single-header, noexception Three CI failures from the initial PR (#5193) addressed: 1. ci_test_clang / ci_test_standards_clang(14): clang's -Werror=exit-time-destructors flagged the Meyers' singleton in detail::empty_json_singleton<>. Switch to a process-lifetime singleton constructed once into properly-aligned storage via placement-new. The destructor is intentionally never invoked at process exit: - the storage is unsigned char[] (trivially destructible) and the cached pointer is a const BasicJsonType* (also trivially destructible), so neither requires an exit-time destructor; - skipping the destructor of an empty array/object constant is safe and avoids any static-destruction-order concerns. 2. ci_test_single_header: the helpers live in an opt-in header that is intentionally not bundled into single_include/nlohmann/json.hpp (matches Step 4 of the discussion in #5129). Guard unit-eval.cpp with JSON_TEST_USING_MULTIPLE_HEADERS so the file is compiled out under the single-header test configuration. 3. ci_test_noexceptions: under JSON_NOEXCEPTION, eval_value(j, key, T{}) must call it->get(); a conversion failure inside from_json becomes std::abort() instead of throwing, so the helper cannot uphold its noexcept contract there. Skip unit-eval.cpp under JSON_NOEXCEPTION; document the limitation explicitly in docs/mkdocs/docs/api/eval.md (a new 'Limitation under JSON_NOEXCEPTION' section). Local re-verification (matrix unchanged from PR description, plus the specific clang warning that broke CI): - MSVC cl (VS 2026), C++17 : 7/7 cases, 79/79 PASS - g++ 15.2, C++11/17/20 : 7/7, 79/79 PASS - clang++ 21.1 + -Werror=exit-time-destructors, C++11/17/20 : 7/7, 79/79 PASS - g++ -fno-exceptions -DJSON_NOEXCEPTION : file skipped cleanly - g++ without JSON_TEST_USING_MULTIPLE_HEADERS=1 (single-header simulation) : file skipped cleanly Signed-off-by: gitpaladin --- docs/mkdocs/docs/api/eval.md | 32 ++++++++++++++++++++++++++------ include/nlohmann/eval.hpp | 28 +++++++++++++++++++++++----- tests/src/unit-eval.cpp | 18 ++++++++++++++++++ 3 files changed, 67 insertions(+), 11 deletions(-) diff --git a/docs/mkdocs/docs/api/eval.md b/docs/mkdocs/docs/api/eval.md index 550d36c053..0f28fd181c 100644 --- a/docs/mkdocs/docs/api/eval.md +++ b/docs/mkdocs/docs/api/eval.md @@ -121,17 +121,37 @@ auto a = eval_value(j, "a", 0); // ADL finds nlohmann::eval_value ## Design notes - These helpers rely only on the **public** API of `basic_json` - (`is_object`, `find`, `end`, `get`, `is_null`, `is_array`, `is_object`) - and `json_pointer::get_checked`. + (`is_object`, `is_array`, `is_null`, `find`, `end`, `get`, + `contains(json_pointer)`, `at(json_pointer)`). - They are intentionally provided as **non-member** functions in an **opt-in** header (``). They are not pulled in by - ``. -- The empty fallback array/object returned by reference is a - `static const` Meyers' singleton, so it is allocated once and is safe - for concurrent reads (thread-safe since C++11). + `` and are not bundled into + `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` warning and any + static-destruction-order concerns. - `ValueType` for `eval_value` is **deduced** from `default_value`, so the common case never requires explicit template arguments. +## Limitation under `JSON_NOEXCEPTION` + +The helpers' `noexcept` guarantee is best-effort under +[`JSON_NOEXCEPTION`](macros/json_noexception.md): + +- `eval_array` / `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 `j[key]` is convertibility-incompatible with `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. + ## See also - [`basic_json::value`](basic_json/value.md) -- exception-throwing diff --git a/include/nlohmann/eval.hpp b/include/nlohmann/eval.hpp index 1a7765b48b..f9d3716343 100644 --- a/include/nlohmann/eval.hpp +++ b/include/nlohmann/eval.hpp @@ -8,6 +8,8 @@ #pragma once +#include // placement new + #include #include @@ -58,14 +60,30 @@ NLOHMANN_JSON_NAMESPACE_BEGIN namespace detail { -// Meyers' singleton holding a static empty JSON value of a given value_t. -// Returning by const reference avoids per-call allocation and is thread-safe -// since C++11. +// 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. template const BasicJsonType& empty_json_singleton() noexcept { - static const BasicJsonType instance(Kind); - return instance; + // POD-like storage: trivially destructible, so it does not itself + // require an exit-time destructor. + alignas(BasicJsonType) static unsigned char storage[sizeof(BasicJsonType)]; + + // 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. + static const BasicJsonType* const instance = + ::new (static_cast(&storage[0])) BasicJsonType(Kind); + + return *instance; } } // namespace detail diff --git a/tests/src/unit-eval.cpp b/tests/src/unit-eval.cpp index 1da43a1b56..4cc826f91a 100644 --- a/tests/src/unit-eval.cpp +++ b/tests/src/unit-eval.cpp @@ -9,6 +9,22 @@ #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; @@ -440,3 +456,5 @@ TEST_CASE("eval functions end-to-end scenario from discussion 5129") CHECK(eval_value(j, "a", 0) == 7); } } + +#endif // JSON_TEST_USING_MULTIPLE_HEADERS && !JSON_NOEXCEPTION From f3fc458719208063ddb0b2cf785560096b2eb515 Mon Sep 17 00:00:00 2001 From: gitpaladin Date: Thu, 28 May 2026 15:29:17 +0800 Subject: [PATCH 3/3] Address CI failures: doc structure & clang-tidy Two CI failures from the second round (https://github.com/nlohmann/json/pull/5193) addressed: 1. ci_test_documentation (ci_test_build_documentation): docs/mkdocs/scripts/check_structure.py enforces that every docs/mkdocs/docs/api/*.md uses only headings from a fixed whitelist (Template parameters / Parameters / Return value / Exception safety / Exceptions / Complexity / Notes / Examples / See also / Version history) and that 'Examples' and 'Version history' are present. Restructure docs/mkdocs/docs/api/eval.md to match: the previous sections (API / Semantics / Comparison with value() / Design notes / Limitation under JSON_NOEXCEPTION) are folded into the standardized layout. The intro signatures move into the leading code block (mirroring operator_literal_json_pointer.md), the semantics/comparison/design/limitation prose moves into 'Notes' admonitions, and per-overload documentation now uses the numbered format (1)-(6). 2. ci_static_analysis_clang (ci_clang_tidy): * bugprone-exception-escape: clang-tidy reported the 'noexcept' functions as potentially throwing. Wrap the j.find(key) calls in the key overloads of eval_value/eval_array/eval_object inside the existing try/catch so the static analyser sees the entire body covered (the pointer overloads were already fully covered). The empty_json_singleton instantiation guard is suppressed locally with a NOLINT band, with a comment explaining why (BasicJsonType's allocator-default ctor is non-throwing for the array/object value_t we instantiate). * cppcoreguidelines-avoid-c-arrays / hicpp-avoid-c-arrays / modernize-avoid-c-arrays: replaced the C-style 'unsigned char storage[N]' with 'std::array', which also addresses the github-advanced-security/Flawfinder 'buffer/char' heuristic raised on the same line. * cppcoreguidelines-owning-memory: NOLINTNEXTLINE on the placement-new line, with a comment marking it as an intentional process-lifetime singleton. Reviewer feedback (gregmarr): * The introductory paragraph now explicitly clarifies that 'null' refers to the JSON value 'null' (or any non-matching JSON shape) rather than to a null C++ object. Local re-verification: * MSVC cl (VS 2026), C++17 : 7/7, 79/79 PASS * g++ 15.2, C++11/17/20 : 7/7, 79/79 PASS * clang++ 21.1 + -Werror=exit-time-destructors + -Wshadow -Wconversion -Wsign-conversion -Wold-style-cast, C++11/17/20 : 7/7, 79/79 PASS Signed-off-by: gitpaladin --- docs/mkdocs/docs/api/eval.md | 298 ++++++++++++++++++++++------------- include/nlohmann/eval.hpp | 51 +++--- 2 files changed, 219 insertions(+), 130 deletions(-) diff --git a/docs/mkdocs/docs/api/eval.md b/docs/mkdocs/docs/api/eval.md index 0f28fd181c..bf19bbbbf9 100644 --- a/docs/mkdocs/docs/api/eval.md +++ b/docs/mkdocs/docs/api/eval.md @@ -2,160 +2,234 @@ ```cpp #include -``` - -Null-safe, `noexcept` accessors for retrieving values from a JSON 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 an object, the -key/pointer is missing, the resolved value is null, or 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`). - -This brings the developer experience of accessing untrusted server-side JSON -closer to JavaScript's optional chaining (`?.`) and nullish coalescing (`??`). - -## API +namespace nlohmann +{ -```cpp -// (1) -- access by key +// (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 by JSON Pointer +// (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) -- array access by key +// (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; +const BasicJsonType& eval_array(const BasicJsonType& j, + const typename BasicJsonType::object_t::key_type& key) noexcept; -// (4) -- array access by JSON Pointer +// (4) -- access a JSON array resolved by @a ptr template -const BasicJsonType& eval_array( - const BasicJsonType& j, - const typename BasicJsonType::json_pointer& ptr) noexcept; +const BasicJsonType& eval_array(const BasicJsonType& j, + const typename BasicJsonType::json_pointer& ptr) noexcept; -// (5) -- object access by key +// (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; +const BasicJsonType& eval_object(const BasicJsonType& j, + const typename BasicJsonType::object_t::key_type& key) noexcept; -// (6) -- object access by JSON Pointer +// (6) -- access a JSON object resolved by @a ptr template -const BasicJsonType& eval_object( - const BasicJsonType& j, - const typename BasicJsonType::json_pointer& ptr) noexcept; -``` +const BasicJsonType& eval_object(const BasicJsonType& j, + const typename BasicJsonType::json_pointer& ptr) noexcept; -## Semantics +} +``` -| Function | On non-object receiver | On missing key/path | On null resolved value | On wrong resolved type | -| ----------------------------- | ---------------------- | ------------------- | ---------------------- | ---------------------- | -| `eval_value(j, key, default)` | returns `default` | returns `default` | returns `default` | returns `default` | -| `eval_value(j, ptr, default)` | returns `default` | returns `default` | returns `default` | returns `default` | -| `eval_array(j, key)` | returns empty `[]` | returns empty `[]` | returns empty `[]` | returns empty `[]` | -| `eval_array(j, ptr)` | returns empty `[]` | returns empty `[]` | returns empty `[]` | returns empty `[]` | -| `eval_object(j, key)` | returns empty `{}` | returns empty `{}` | returns empty `{}` | returns empty `{}` | -| `eval_object(j, ptr)` | returns empty `{}` | returns empty `{}` | returns empty `{}` | returns empty `{}` | +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. -All overloads are `noexcept`. They never throw regardless of the receiver's -type or the structure of the JSON value. +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). -## Comparison with `value()` +This brings the developer experience of accessing untrusted server-side JSON +closer to JavaScript's optional chaining (`?.`) and nullish coalescing (`??`). -| 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 `null` | **throws `type_error`** | returns default | -| `j` is array, string, number, bool, ... | **throws `type_error`** | returns default | -| Resolved value is `null` | returns null-converted value | returns default | +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 -### Safe access on a possibly-null payload +??? example "Example: safe access on a possibly-null payload" -```cpp -#include -#include + The example below demonstrates safe defensive access to a JSON value + received from an untrusted source. -using nlohmann::json; + ```cpp + #include + #include -auto received = from_server(); // might be null, partial, or wrong type + using nlohmann::json; -int a = nlohmann::eval_value(received, "a", 0); -auto d = nlohmann::eval_value(received, - json::json_pointer("/c/d"), - std::string{}); + // a payload received from a remote source -- could be JSON null, + // partial, of the wrong type, or fully populated + json received = nullptr; -for (const auto& item : nlohmann::eval_array(received, "items")) -{ - // safe, no need to check is_object() / contains() / is_array() -} + int a = nlohmann::eval_value(received, "a", 0); + auto d = nlohmann::eval_value(received, + json::json_pointer("/c/d"), + std::string{}); -for (const auto& [k, v] : nlohmann::eval_object(received, "metadata").items()) -{ - // safe, no exceptions -} -``` + // 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; + } -### ADL + // safe iteration over a nested object + for (const auto& kv : nlohmann::eval_object(received, "metadata").items()) + { + // process each key/value pair + (void) kv; + } + ``` -Because `eval_*` lives in `namespace nlohmann`, it is found by -argument-dependent lookup -- you can omit the namespace qualifier: +??? example "Example: ADL allows omitting the namespace qualifier" -```cpp -const json j = {{"a", 7}}; -auto a = eval_value(j, "a", 0); // ADL finds nlohmann::eval_value -``` + Because the helpers live in `namespace nlohmann`, they are found by + argument-dependent lookup, so the explicit qualifier is optional in + user code: -## Design notes - -- These helpers rely only on the **public** API of `basic_json` - (`is_object`, `is_array`, `is_null`, `find`, `end`, `get`, - `contains(json_pointer)`, `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 - `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` warning and any - static-destruction-order concerns. -- `ValueType` for `eval_value` is **deduced** from `default_value`, so - the common case never requires explicit template arguments. - -## Limitation under `JSON_NOEXCEPTION` - -The helpers' `noexcept` guarantee is best-effort under -[`JSON_NOEXCEPTION`](macros/json_noexception.md): - -- `eval_array` / `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 `j[key]` is convertibility-incompatible with `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. + ```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. diff --git a/include/nlohmann/eval.hpp b/include/nlohmann/eval.hpp index f9d3716343..e05d16f245 100644 --- a/include/nlohmann/eval.hpp +++ b/include/nlohmann/eval.hpp @@ -8,7 +8,8 @@ #pragma once -#include // placement new +#include // array, used for aligned storage of the empty singletons +#include // placement new #include #include @@ -70,21 +71,30 @@ namespace detail // 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 { - // POD-like storage: trivially destructible, so it does not itself - // require an exit-time destructor. - alignas(BasicJsonType) static unsigned char storage[sizeof(BasicJsonType)]; + // 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[0])) BasicJsonType(Kind); + ::new (static_cast(storage.data())) BasicJsonType(Kind); return *instance; } +// NOLINTEND(bugprone-exception-escape) } // namespace detail @@ -111,18 +121,15 @@ ValueType eval_value(const BasicJsonType& j, return default_value; } - const auto it = j.find(key); - if (it != j.end() && !it->is_null()) + NLOHMANN_EVAL_TRY { - 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; - } } + NLOHMANN_EVAL_CATCH_ALL {} return default_value; } @@ -190,11 +197,15 @@ const BasicJsonType& eval_array( return empty; } - const auto it = j.find(key); - if (it != j.end() && it->is_array()) + NLOHMANN_EVAL_TRY { - return *it; + const auto it = j.find(key); + if (it != j.end() && it->is_array()) + { + return *it; + } } + NLOHMANN_EVAL_CATCH_ALL {} return empty; } @@ -262,11 +273,15 @@ const BasicJsonType& eval_object( return empty; } - const auto it = j.find(key); - if (it != j.end() && it->is_object()) + NLOHMANN_EVAL_TRY { - return *it; + const auto it = j.find(key); + if (it != j.end() && it->is_object()) + { + return *it; + } } + NLOHMANN_EVAL_CATCH_ALL {} return empty; }