Skip to content

Add null-safe noexcept accessors eval_value/eval_array/eval_object#5193

Open
gitpaladin wants to merge 2 commits into
nlohmann:developfrom
gitpaladin:feature/eval-accessors
Open

Add null-safe noexcept accessors eval_value/eval_array/eval_object#5193
gitpaladin wants to merge 2 commits into
nlohmann:developfrom
gitpaladin:feature/eval-accessors

Conversation

@gitpaladin
Copy link
Copy Markdown

Summary

Introduces an opt-in header <nlohmann/eval.hpp> with three free function templates that complement basic_json::value() with semantics designed for untrusted JSON payloads:

Function Behaviour
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 a const& to a static empty array on any non-matching condition.
eval_object(j, key|ptr) noexcept, returns a const& to a static empty object on any non-matching condition.

Follows the design agreed in #5129.

Why non-member, opt-in header

Per your preference in the discussion ("I definitely prefer this over an extension of the (already bloated) basic_json API"):

  • Non-member functions in namespace nlohmann so ADL works without explicit qualification (eval_value(j, ...) just works).
  • Dedicated opt-in header — basic_json's public surface is not extended.
  • Uses only the public API (is_object, is_array, is_null, find, end, get, contains(json_pointer), at(json_pointer)).
  • Pointer overloads guard with j.contains(ptr) before j.at(ptr), so they remain correct under JSON_NOEXCEPTION too (where at() would otherwise abort).
  • Static empty array/object fallbacks are returned by const reference via Meyers' singletons — no per-call allocation, thread-safe since C++11.
  • A header-private NLOHMANN_EVAL_TRY/NLOHMANN_EVAL_CATCH_ALL pair mirrors JSON_TRY/JSON_INTERNAL_CATCH, since those macros are #undef'd at the end of json.hpp via macro_unscope.hpp and are not visible to consumers.

What is added

  • include/nlohmann/eval.hpp — 6 overloads (key / json_pointer × value / array / object).
  • tests/src/unit-eval.cpp — 7 test cases, 79 assertions, picked up automatically by the existing file(GLOB src/unit-*.cpp) in tests/CMakeLists.txt.
  • docs/mkdocs/docs/api/eval.md — API page mirroring basic_json/value.md style.

What is not changed

Local test matrix

Toolchain Standards Result
MSVC cl (VS 2026) C++17 7/7 cases, 79/79 assertions PASS, 0 warnings
MSYS2 g++ 15.2 C++11 / C++17 / C++20 PASS each, 0 warnings
MSYS2 clang++ 21.1 C++11 / C++17 / C++20 PASS each, 0 warnings (incl. -Wshadow -Wconversion -Wsign-conversion -Wold-style-cast)
g++ 15.2 + -fno-exceptions -DJSON_NOEXCEPTION C++17 smoke test PASS (doctest's own limitations apply to the full suite, same as test-disabled_exceptions)

Checklist

  • Changes described above.
  • References discussion Feature Discussion: Null-safe, noexcept accessors — `eval_value`, `eval_array`, `eval_object` #5129.
  • New code covered by tests in tests/src/unit-eval.cpp (7 cases / 79 assertions).
  • API documentation page added under docs/mkdocs/docs/api/.
  • make amalgamate — not run yet because the new header is intentionally not part of the single-include. If you'd like it bundled, I'll extend the amalgamate config and rerun. Will also rerun should the CI's amalgamation check flag any astyle drift.

Marked as draft so you can pre-screen the direction; happy to address feedback (naming, single-include bundling, anything else) before un-drafting.

Introduce a small, opt-in header <nlohmann/eval.hpp> 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 nlohmann#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 <alexander.wuhan@gmail.com>
Three CI failures from the initial PR (nlohmann#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 nlohmann#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<T>(); 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 <alexander.wuhan@gmail.com>
Comment thread include/nlohmann/eval.hpp
{
// POD-like storage: trivially destructible, so it does not itself
// require an exit-time destructor.
alignas(BasicJsonType) static unsigned char storage[sizeof(BasicJsonType)];
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants