From cc6d74feb0ea63482e659aaa5edc14033befd515 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Tue, 29 Sep 2026 23:41:48 +0200 Subject: [PATCH] Copy a pair-shaped array value under JSON_BRACE_INIT_COPY_SEMANTICS With JSON_BRACE_INIT_COPY_SEMANTICS enabled, single-element brace initialization from a JSON value decided whether to copy the value or build an object by inspecting the value's runtime shape: a two-element array whose first element is a string, such as ["key", 42], was turned into an object instead of being copied. This made the behavior depend on the element's content, and it did not distinguish an existing value of this shape from a nested braced pair written in the source, such as the inner {"key", "value"} of {{"key", "value"}}. json_ref now records whether it was constructed from a braced list (true only for the std::initializer_list constructor used for nested braced lists) or from a value. The initializer-list constructor uses this to copy or move a single non-braced-list element before deciding whether the list describes an object, so a JSON value is always copied regardless of its shape, while a braced pair written in the source still creates an object. Fixes #5662. Signed-off-by: Niels Lohmann --- .../macros/json_brace_init_copy_semantics.md | 8 ++++--- include/nlohmann/detail/json_ref.hpp | 9 ++++++++ include/nlohmann/json.hpp | 12 ++++++++++ single_include/nlohmann/json.hpp | 21 ++++++++++++++++++ tests/src/unit-brace-init-copy-semantics.cpp | 22 +++++++++++++++++++ 5 files changed, 69 insertions(+), 3 deletions(-) diff --git a/docs/mkdocs/docs/api/macros/json_brace_init_copy_semantics.md b/docs/mkdocs/docs/api/macros/json_brace_init_copy_semantics.md index 2301a0486..3ee2d5ef8 100644 --- a/docs/mkdocs/docs/api/macros/json_brace_init_copy_semantics.md +++ b/docs/mkdocs/docs/api/macros/json_brace_init_copy_semantics.md @@ -50,9 +50,11 @@ The default value is `0` (disabled — existing behavior is preserved). ``` Code that relies on these producing arrays must use `json::array()` instead (see below). Lists with more than one - element, and a single `[string, value]` pair such as `{{"key", "value"}}`, which still creates an object, are not - affected. The library's own conversions are not affected either: for example, `std::tuple{5}` still becomes - `[5]`. + element, and a single `[string, value]` pair *written as a braced list*, such as `{{"key", "value"}}`, which still + creates an object, are not affected. This exception is based on how the pair is written, not on the shape of its + value: an existing JSON value that happens to be a two-element array with a string as its first element, such as + `json arr = {"key", 42};`, is still copied by `json j{arr};` rather than turned into an object. The library's own + conversions are not affected either: for example, `std::tuple{5}` still becomes `[5]`. !!! note "ABI compatibility" diff --git a/include/nlohmann/detail/json_ref.hpp b/include/nlohmann/detail/json_ref.hpp index 0d52b0a62..b270c9c54 100644 --- a/include/nlohmann/detail/json_ref.hpp +++ b/include/nlohmann/detail/json_ref.hpp @@ -34,6 +34,7 @@ class json_ref json_ref(std::initializer_list init) : owned_value(init) + , braced_list(true) {} template < @@ -69,9 +70,17 @@ class json_ref return &** this; } + /// whether the value was written as a braced list, such as {"key", 1}, + /// rather than given as a value + bool is_braced_list() const noexcept + { + return braced_list; + } + private: mutable value_type owned_value = nullptr; value_type const* value_ref = nullptr; + bool braced_list = false; }; } // namespace detail diff --git a/include/nlohmann/json.hpp b/include/nlohmann/json.hpp index 500fcddf2..37436be54 100644 --- a/include/nlohmann/json.hpp +++ b/include/nlohmann/json.hpp @@ -1672,6 +1672,18 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec bool type_deduction = true, value_t manual_type = value_t::array) { +#if JSON_BRACE_INIT_COPY_SEMANTICS + // a single element that is a value rather than a braced list is + // copied or moved as is, whatever its content looks like + if (type_deduction && init.size() == 1 && !init.begin()->is_braced_list()) + { + *this = init.begin()->moved_or_copied(); + set_parents(); + assert_invariant(); + return; + } +#endif + // check if each element is an array with two elements whose first // element is a string bool is_an_object = std::all_of(init.begin(), init.end(), diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 576498738..e6114da89 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -20047,6 +20047,7 @@ class json_ref json_ref(std::initializer_list init) : owned_value(init) + , braced_list(true) {} template < @@ -20082,9 +20083,17 @@ class json_ref return &** this; } + /// whether the value was written as a braced list, such as {"key", 1}, + /// rather than given as a value + bool is_braced_list() const noexcept + { + return braced_list; + } + private: mutable value_type owned_value = nullptr; value_type const* value_ref = nullptr; + bool braced_list = false; }; } // namespace detail @@ -27753,6 +27762,18 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec bool type_deduction = true, value_t manual_type = value_t::array) { +#if JSON_BRACE_INIT_COPY_SEMANTICS + // a single element that is a value rather than a braced list is + // copied or moved as is, whatever its content looks like + if (type_deduction && init.size() == 1 && !init.begin()->is_braced_list()) + { + *this = init.begin()->moved_or_copied(); + set_parents(); + assert_invariant(); + return; + } +#endif + // check if each element is an array with two elements whose first // element is a string bool is_an_object = std::all_of(init.begin(), init.end(), diff --git a/tests/src/unit-brace-init-copy-semantics.cpp b/tests/src/unit-brace-init-copy-semantics.cpp index 1ee0c6607..e3406758b 100644 --- a/tests/src/unit-brace-init-copy-semantics.cpp +++ b/tests/src/unit-brace-init-copy-semantics.cpp @@ -72,6 +72,28 @@ TEST_CASE("JSON_BRACE_INIT_COPY_SEMANTICS") CHECK(j7 == json::array({1, 2})); } + SECTION("single-element brace initialization copies a pair-shaped array value (#5662)") + { + // a JSON value that happens to be a 2-element array whose first + // element is a string must still be copied, not turned into an + // object; only a braced list written in the source, such as the + // inner {"key", "value"} of {{"key", "value"}}, describes an object + json const pair_shaped = json::array({"key", 42}); + + json const j1{pair_shaped}; + CHECK(j1.is_array()); + CHECK(j1 == pair_shaped); + + json const j2 = {pair_shaped}; + CHECK(j2.is_array()); + CHECK(j2 == pair_shaped); + + // the same holds for an rvalue of the same shape + json const j3{json::array({"key", 42})}; + CHECK(j3.is_array()); + CHECK(j3 == pair_shaped); + } + SECTION("what the macro does not change") { // lists with more than one element are unaffected