From 43e8181d78c023bdbc0d0ac745cfbd8834ae74e0 Mon Sep 17 00:00:00 2001 From: Samaresh Kumar Singh Date: Mon, 2 Mar 2026 20:49:29 -0600 Subject: [PATCH 1/9] fix: treat single-element brace-init as copy/move When passing a json value using brace initialization with a single element (e.g., `json j{someObj}` or `foo({someJson})`), C++ always prefers the initializer_list constructor over the copy/move constructor. This caused the value to be unexpectedly wrapped in a single-element array. This bug was previously compiler-dependent (GCC wrapped, Clang did not), but Clang 20 started matching GCC behavior, making it a universal issue. Fix: In the initializer_list constructor, when type deduction is enabled and the list has exactly one element, copy/move it directly instead of creating a single-element array. Before: json obj = {{"key", 1}}; json j{obj}; // -> [{"key":1}] (wrong: array) foo({obj}); // -> [{"key":1}] (wrong: array) After: json j{obj}; // -> {"key":1} (correct: copy) foo({obj}); // -> {"key":1} (correct: copy) To explicitly create a single-element array, use json::array({value}). Fixes the issue #5074 Signed-off-by: Samaresh Kumar Singh --- docs/mkdocs/docs/home/faq.md | 19 +++++-- include/nlohmann/json.hpp | 12 +++++ single_include/nlohmann/json.hpp | 8 +++ tests/src/unit-class_parser.cpp | 2 +- ...unit-class_parser_diagnostic_positions.cpp | 2 +- tests/src/unit-constructor1.cpp | 41 +++++++------- tests/src/unit-modifiers.cpp | 2 +- tests/src/unit-regression2.cpp | 53 ++++++++++++++++++- 8 files changed, 113 insertions(+), 26 deletions(-) diff --git a/docs/mkdocs/docs/home/faq.md b/docs/mkdocs/docs/home/faq.md index 74709bac68..5a1d354a71 100644 --- a/docs/mkdocs/docs/home/faq.md +++ b/docs/mkdocs/docs/home/faq.md @@ -20,7 +20,16 @@ yield different results (`#!json [true]` vs. `#!json true`)? -This is a known issue, and -- even worse -- the behavior differs between GCC and Clang. The "culprit" for this is the library's constructor overloads for initializer lists to allow syntax like +Starting from this version, single-value brace initialization is treated as copy/move instead of wrapping in a single-element array: + +```cpp +json j_orig = {{"key", "value"}}; + +json j{j_orig}; +j_orig.method(); +``` + +The root cause was the library's constructor overloads for initializer lists, which allow syntax like ```cpp json array = {1, 2, 3, 4}; @@ -32,11 +41,13 @@ for arrays and json object = {{"one", 1}, {"two", 2}}; ``` -for objects. +for objects. Because C++ always prefers `initializer_list` constructors for brace initialization, a single-element brace-init `json j{someValue}` previously wrapped the value in an array instead of copying it. -!!! tip +After the fix, a single-element brace-init behaves like copy/move initialization. To explicitly create a single-element array, use `json::array({value})`. + +!!! note - To avoid any confusion and ensure portable code, **do not** use brace initialization with the types `basic_json`, `json`, or `ordered_json` unless you want to create an object or array as shown in the examples above. +The fix changes the behavior of single-element brace initialization: `json j{x}` is now equivalent to `json j(x)` (copy/move) rather than `json j = json::array({x})` (single-element array). Code relying on the old behavior should be updated to use `json::array({x})` explicitly. ## Limitations diff --git a/include/nlohmann/json.hpp b/include/nlohmann/json.hpp index d4502a5c96..aeeb33a0de 100644 --- a/include/nlohmann/json.hpp +++ b/include/nlohmann/json.hpp @@ -955,6 +955,18 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec } else { + // if there is exactly one element and type deduction is enabled, + // treat it as copy/move to avoid unintentionally wrapping a single + // json value in a single-element array + // (see https://github.com/nlohmann/json/issues/5074) + if (type_deduction && init.size() == 1) + { + *this = init.begin()->moved_or_copied(); + set_parents(); + assert_invariant(); + return; + } + // the initializer list describes an array -> create an array m_data.m_type = value_t::array; m_data.m_value.array = create(init.begin(), init.end()); diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index ceb7a9f11d..b18acee3fd 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -21074,6 +21074,14 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec } else { + if (type_deduction && init.size() == 1) + { + *this = init.begin()->moved_or_copied(); + set_parents(); + assert_invariant(); + return; + } + // the initializer list describes an array -> create an array m_data.m_type = value_t::array; m_data.m_value.array = create(init.begin(), init.end()); diff --git a/tests/src/unit-class_parser.cpp b/tests/src/unit-class_parser.cpp index f41d8efa0c..e42ac17cd8 100644 --- a/tests/src/unit-class_parser.cpp +++ b/tests/src/unit-class_parser.cpp @@ -1506,7 +1506,7 @@ TEST_CASE("parser class") // removed all objects in array. CHECK (j_filtered2.size() == 1); - CHECK (j_filtered2 == json({1})); + CHECK (j_filtered2 == json::array({1})); } SECTION("filter specific events") diff --git a/tests/src/unit-class_parser_diagnostic_positions.cpp b/tests/src/unit-class_parser_diagnostic_positions.cpp index f09ec0f6f4..ed043d3e24 100644 --- a/tests/src/unit-class_parser_diagnostic_positions.cpp +++ b/tests/src/unit-class_parser_diagnostic_positions.cpp @@ -1517,7 +1517,7 @@ TEST_CASE("parser class") // removed all objects in array. CHECK (j_filtered2.size() == 1); - CHECK (j_filtered2 == json({1})); + CHECK (j_filtered2 == json::array({1})); } SECTION("filter specific events") diff --git a/tests/src/unit-constructor1.cpp b/tests/src/unit-constructor1.cpp index 8eb7d6ab55..098c0ab425 100644 --- a/tests/src/unit-constructor1.cpp +++ b/tests/src/unit-constructor1.cpp @@ -283,7 +283,8 @@ TEST_CASE("constructors") SECTION("std::pair/tuple/array failures") { - json const j{1}; + // Use json::array() to explicitly create a single-element array + json const j = json::array({1}); CHECK_THROWS_WITH_AS((j.get>()), "[json.exception.out_of_range.401] array index 1 is out of range", json::out_of_range&); CHECK_THROWS_WITH_AS((j.get>()), "[json.exception.out_of_range.401] array index 1 is out of range", json::out_of_range&); @@ -935,14 +936,18 @@ TEST_CASE("constructors") { SECTION("explicit") { + // j is a copy of the empty array element, not [[]] json const j(json::initializer_list_t {json(json::array_t())}); CHECK(j.type() == json::value_t::array); + CHECK(j.empty()); } SECTION("implicit") { + // j is a copy of the empty array element, not [[]] json const j {json::array_t()}; CHECK(j.type() == json::value_t::array); + CHECK(j.empty()); } } @@ -951,13 +956,13 @@ TEST_CASE("constructors") SECTION("explicit") { json const j(json::initializer_list_t {json(json::object_t())}); - CHECK(j.type() == json::value_t::array); + CHECK(j.type() == json::value_t::object); } SECTION("implicit") { json const j {json::object_t()}; - CHECK(j.type() == json::value_t::array); + CHECK(j.type() == json::value_t::object); } } @@ -966,13 +971,13 @@ TEST_CASE("constructors") SECTION("explicit") { json const j(json::initializer_list_t {json("Hello world")}); - CHECK(j.type() == json::value_t::array); + CHECK(j.type() == json::value_t::string); } SECTION("implicit") { json const j {"Hello world"}; - CHECK(j.type() == json::value_t::array); + CHECK(j.type() == json::value_t::string); } } @@ -981,13 +986,13 @@ TEST_CASE("constructors") SECTION("explicit") { json const j(json::initializer_list_t {json(true)}); - CHECK(j.type() == json::value_t::array); + CHECK(j.type() == json::value_t::boolean); } SECTION("implicit") { json const j {true}; - CHECK(j.type() == json::value_t::array); + CHECK(j.type() == json::value_t::boolean); } } @@ -996,13 +1001,13 @@ TEST_CASE("constructors") SECTION("explicit") { json const j(json::initializer_list_t {json(1)}); - CHECK(j.type() == json::value_t::array); + CHECK(j.type() == json::value_t::number_integer); } SECTION("implicit") { json const j {1}; - CHECK(j.type() == json::value_t::array); + CHECK(j.type() == json::value_t::number_integer); } } @@ -1011,13 +1016,13 @@ TEST_CASE("constructors") SECTION("explicit") { json const j(json::initializer_list_t {json(1u)}); - CHECK(j.type() == json::value_t::array); + CHECK(j.type() == json::value_t::number_unsigned); } SECTION("implicit") { json const j {1u}; - CHECK(j.type() == json::value_t::array); + CHECK(j.type() == json::value_t::number_unsigned); } } @@ -1026,13 +1031,13 @@ TEST_CASE("constructors") SECTION("explicit") { json const j(json::initializer_list_t {json(42.23)}); - CHECK(j.type() == json::value_t::array); + CHECK(j.type() == json::value_t::number_float); } SECTION("implicit") { json const j {42.23}; - CHECK(j.type() == json::value_t::array); + CHECK(j.type() == json::value_t::number_float); } } } @@ -1110,7 +1115,7 @@ TEST_CASE("constructors") std::string source(1024, '!'); const auto* source_addr = source.data(); json j = {std::move(source)}; - const auto* target_addr = j[0].get_ref().data(); + const auto* target_addr = j.get_ref().data(); const bool success = (target_addr == source_addr); CHECK(success); } @@ -1145,7 +1150,7 @@ TEST_CASE("constructors") json::array_t source = {1, 2, 3}; const auto* source_addr = source.data(); json j {std::move(source)}; - const auto* target_addr = j[0].get_ref().data(); + const auto* target_addr = j.get_ref().data(); const bool success = (target_addr == source_addr); CHECK(success); } @@ -1165,7 +1170,7 @@ TEST_CASE("constructors") json::array_t source = {1, 2, 3}; const auto* source_addr = source.data(); json j = {std::move(source)}; - const auto* target_addr = j[0].get_ref().data(); + const auto* target_addr = j.get_ref().data(); const bool success = (target_addr == source_addr); CHECK(success); } @@ -1188,7 +1193,7 @@ TEST_CASE("constructors") json::object_t source = {{"hello", "world"}}; const json* source_addr = &source.at("hello"); json j {std::move(source)}; - CHECK(&(j[0].get_ref().at("hello")) == source_addr); + CHECK(&(j.get_ref().at("hello")) == source_addr); } SECTION("constructor with implicit types (object)") @@ -1204,7 +1209,7 @@ TEST_CASE("constructors") json::object_t source = {{"hello", "world"}}; const json* source_addr = &source.at("hello"); json j = {std::move(source)}; - CHECK(&(j[0].get_ref().at("hello")) == source_addr); + CHECK(&(j.get_ref().at("hello")) == source_addr); } SECTION("assignment with implicit types (object)") diff --git a/tests/src/unit-modifiers.cpp b/tests/src/unit-modifiers.cpp index bf9bd83503..9821b756a6 100644 --- a/tests/src/unit-modifiers.cpp +++ b/tests/src/unit-modifiers.cpp @@ -310,7 +310,7 @@ TEST_CASE("modifiers") auto& x = j.emplace_back(3, "foo"); CHECK(x == json({"foo", "foo", "foo"})); CHECK(j.type() == json::value_t::array); - CHECK(j == json({{"foo", "foo", "foo"}})); + CHECK(j == json::array({json({"foo", "foo", "foo"})})); } } diff --git a/tests/src/unit-regression2.cpp b/tests/src/unit-regression2.cpp index 5ef7da59d4..36747489b7 100644 --- a/tests/src/unit-regression2.cpp +++ b/tests/src/unit-regression2.cpp @@ -844,7 +844,7 @@ TEST_CASE("regression tests 2") SECTION("std::tuple") { { - const json j = {9}; + const json j = json::array({9}); auto t = j.get>(); CHECK(std::get<0>(t).x == 9); } @@ -1119,6 +1119,57 @@ TEST_CASE("regression tests 2") CHECK((decoded == json_4804::array())); } #endif + + SECTION("copy constructor changes semantics with brace initialization") + { + // object + { + json const j_obj = {{"key", "value"}, {"num", 42}}; + json const j{j_obj}; + CHECK(j.is_object()); + CHECK(j == j_obj); + } + + // array + { + json const j_arr = {1, 2, 3}; + json const j{j_arr}; + CHECK(j.is_array()); + CHECK(j == j_arr); + } + + // primitives + { + json const j_bool{true}; + CHECK(j_bool.is_boolean()); + + json const j_num{42}; + CHECK(j_num.is_number_integer()); + + json const j_float{3.14}; + CHECK(j_float.is_number_float()); + + json const j_str{"hello"}; + CHECK(j_str.is_string()); + } + + // passing by-value should still work correctly + { + auto receive_by_value = [](json j) { return j; }; + json const j_obj = {{"key", "value"}}; + json const result = receive_by_value(j_obj); + CHECK(result.is_object()); + CHECK(result == j_obj); + } + + // explicitly creating a single-element array still works via json::array() + { + json const j_arr = json::array({42}); + CHECK(j_arr.is_array()); + CHECK(j_arr.size() == 1); + CHECK(j_arr[0] == 42); + } + } } TEST_CASE_TEMPLATE("issue #4798 - nlohmann::json::to_msgpack() encode float NaN as double", T, double, float) // NOLINT(readability-math-missing-parentheses, bugprone-throwing-static-initialization) From 6c1bee0d6c31c498e34331fd89f3ad59e395cb17 Mon Sep 17 00:00:00 2001 From: Samaresh Kumar Singh Date: Mon, 2 Mar 2026 21:03:46 -0600 Subject: [PATCH 2/9] fix: regenerate amalgamated single_include/nlohmann/json.hpp - Add missing comment from include/nlohmann/json.hpp explaining the single-element brace-init fix (issue #5074) - Fix extra 4-space indentation in embedded json_fwd.hpp section Regenerated by running: make amalgamate Signed-off-by: Samaresh Kumar Singh --- single_include/nlohmann/json.hpp | 130 ++++++++++++++++--------------- 1 file changed, 67 insertions(+), 63 deletions(-) diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index b18acee3fd..4eb892837b 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -3499,71 +3499,71 @@ NLOHMANN_JSON_NAMESPACE_END // SPDX-License-Identifier: MIT #ifndef INCLUDE_NLOHMANN_JSON_FWD_HPP_ - #define INCLUDE_NLOHMANN_JSON_FWD_HPP_ +#define INCLUDE_NLOHMANN_JSON_FWD_HPP_ - #include // int64_t, uint64_t - #include // map - #include // allocator - #include // string - #include // vector +#include // int64_t, uint64_t +#include // map +#include // allocator +#include // string +#include // vector - // #include +// #include - /*! - @brief namespace for Niels Lohmann - @see https://github.com/nlohmann - @since version 1.0.0 - */ - NLOHMANN_JSON_NAMESPACE_BEGIN +/*! +@brief namespace for Niels Lohmann +@see https://github.com/nlohmann +@since version 1.0.0 +*/ +NLOHMANN_JSON_NAMESPACE_BEGIN - /*! - @brief default JSONSerializer template argument +/*! +@brief default JSONSerializer template argument - This serializer ignores the template arguments and uses ADL - ([argument-dependent lookup](https://en.cppreference.com/w/cpp/language/adl)) - for serialization. - */ - template - struct adl_serializer; - - /// a class to store JSON values - /// @sa https://json.nlohmann.me/api/basic_json/ - template class ObjectType = - std::map, - template class ArrayType = std::vector, - class StringType = std::string, class BooleanType = bool, - class NumberIntegerType = std::int64_t, - class NumberUnsignedType = std::uint64_t, - class NumberFloatType = double, - template class AllocatorType = std::allocator, - template class JSONSerializer = - adl_serializer, - class BinaryType = std::vector, // cppcheck-suppress syntaxError - class CustomBaseClass = void> - class basic_json; - - /// @brief JSON Pointer defines a string syntax for identifying a specific value within a JSON document - /// @sa https://json.nlohmann.me/api/json_pointer/ - template - class json_pointer; +This serializer ignores the template arguments and uses ADL +([argument-dependent lookup](https://en.cppreference.com/w/cpp/language/adl)) +for serialization. +*/ +template +struct adl_serializer; + +/// a class to store JSON values +/// @sa https://json.nlohmann.me/api/basic_json/ +template class ObjectType = + std::map, + template class ArrayType = std::vector, + class StringType = std::string, class BooleanType = bool, + class NumberIntegerType = std::int64_t, + class NumberUnsignedType = std::uint64_t, + class NumberFloatType = double, + template class AllocatorType = std::allocator, + template class JSONSerializer = + adl_serializer, + class BinaryType = std::vector, // cppcheck-suppress syntaxError + class CustomBaseClass = void> +class basic_json; - /*! - @brief default specialization - @sa https://json.nlohmann.me/api/json/ - */ - using json = basic_json<>; +/// @brief JSON Pointer defines a string syntax for identifying a specific value within a JSON document +/// @sa https://json.nlohmann.me/api/json_pointer/ +template +class json_pointer; - /// @brief a minimal map-like container that preserves insertion order - /// @sa https://json.nlohmann.me/api/ordered_map/ - template - struct ordered_map; +/*! +@brief default specialization +@sa https://json.nlohmann.me/api/json/ +*/ +using json = basic_json<>; - /// @brief specialization that maintains the insertion order of object keys - /// @sa https://json.nlohmann.me/api/ordered_json/ - using ordered_json = basic_json; +/// @brief a minimal map-like container that preserves insertion order +/// @sa https://json.nlohmann.me/api/ordered_map/ +template +struct ordered_map; - NLOHMANN_JSON_NAMESPACE_END +/// @brief specialization that maintains the insertion order of object keys +/// @sa https://json.nlohmann.me/api/ordered_json/ +using ordered_json = basic_json; + +NLOHMANN_JSON_NAMESPACE_END #endif // INCLUDE_NLOHMANN_JSON_FWD_HPP_ @@ -5427,7 +5427,7 @@ NLOHMANN_JSON_NAMESPACE_END // #include -// JSON_HAS_CPP_17 + // JSON_HAS_CPP_17 #ifdef JSON_HAS_CPP_17 #include // optional #endif @@ -20255,10 +20255,10 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec const bool allow_exceptions = true, const bool ignore_comments = false, const bool ignore_trailing_commas = false - ) + ) { return ::nlohmann::detail::parser(std::move(adapter), - std::move(cb), allow_exceptions, ignore_comments, ignore_trailing_commas); + std::move(cb), allow_exceptions, ignore_comments, ignore_trailing_commas); } private: @@ -20956,8 +20956,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec detail::enable_if_t < !detail::is_basic_json::value && detail::is_compatible_type::value, int > = 0 > basic_json(CompatibleType && val) noexcept(noexcept( // NOLINT(bugprone-forwarding-reference-overload,bugprone-exception-escape) - JSONSerializer::to_json(std::declval(), - std::forward(val)))) + JSONSerializer::to_json(std::declval(), + std::forward(val)))) { JSONSerializer::to_json(*this, std::forward(val)); set_parents(); @@ -21074,6 +21074,10 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec } else { + // if there is exactly one element and type deduction is enabled, + // treat it as copy/move to avoid unintentionally wrapping a single + // json value in a single-element array + // (see https://github.com/nlohmann/json/issues/5074) if (type_deduction && init.size() == 1) { *this = init.begin()->moved_or_copied(); @@ -21759,7 +21763,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec detail::has_from_json::value, int > = 0 > ValueType get_impl(detail::priority_tag<0> /*unused*/) const noexcept(noexcept( - JSONSerializer::from_json(std::declval(), std::declval()))) + JSONSerializer::from_json(std::declval(), std::declval()))) { auto ret = ValueType(); JSONSerializer::from_json(*this, ret); @@ -21801,7 +21805,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec detail::has_non_default_from_json::value, int > = 0 > ValueType get_impl(detail::priority_tag<1> /*unused*/) const noexcept(noexcept( - JSONSerializer::from_json(std::declval()))) + JSONSerializer::from_json(std::declval()))) { return JSONSerializer::from_json(*this); } @@ -21951,7 +21955,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec detail::has_from_json::value, int > = 0 > ValueType & get_to(ValueType& v) const noexcept(noexcept( - JSONSerializer::from_json(std::declval(), v))) + JSONSerializer::from_json(std::declval(), v))) { JSONSerializer::from_json(*this, v); return v; From eadcd26d5fe0479b6daae7d569044d47502f0615 Mon Sep 17 00:00:00 2001 From: Samaresh Kumar Singh Date: Wed, 1 Apr 2026 20:07:21 -0500 Subject: [PATCH 3/9] Revert brace-init semantics change and fix amalgamation The single-element brace-init change was a breaking change that cannot be accepted upstream. Reverted all related source, test, and doc changes, then regenerated single_include with correct indentation to pass the amalgamation CI check. Signed-off-by: Samaresh Kumar Singh --- docs/mkdocs/docs/home/faq.md | 19 +-- include/nlohmann/json.hpp | 12 -- single_include/nlohmann/json.hpp | 138 ++++++++---------- tests/src/unit-class_parser.cpp | 2 +- ...unit-class_parser_diagnostic_positions.cpp | 2 +- tests/src/unit-constructor1.cpp | 41 +++--- tests/src/unit-modifiers.cpp | 2 +- tests/src/unit-regression2.cpp | 53 +------ 8 files changed, 89 insertions(+), 180 deletions(-) diff --git a/docs/mkdocs/docs/home/faq.md b/docs/mkdocs/docs/home/faq.md index 5a1d354a71..74709bac68 100644 --- a/docs/mkdocs/docs/home/faq.md +++ b/docs/mkdocs/docs/home/faq.md @@ -20,16 +20,7 @@ yield different results (`#!json [true]` vs. `#!json true`)? -Starting from this version, single-value brace initialization is treated as copy/move instead of wrapping in a single-element array: - -```cpp -json j_orig = {{"key", "value"}}; - -json j{j_orig}; -j_orig.method(); -``` - -The root cause was the library's constructor overloads for initializer lists, which allow syntax like +This is a known issue, and -- even worse -- the behavior differs between GCC and Clang. The "culprit" for this is the library's constructor overloads for initializer lists to allow syntax like ```cpp json array = {1, 2, 3, 4}; @@ -41,13 +32,11 @@ for arrays and json object = {{"one", 1}, {"two", 2}}; ``` -for objects. Because C++ always prefers `initializer_list` constructors for brace initialization, a single-element brace-init `json j{someValue}` previously wrapped the value in an array instead of copying it. - -After the fix, a single-element brace-init behaves like copy/move initialization. To explicitly create a single-element array, use `json::array({value})`. +for objects. -!!! note +!!! tip -The fix changes the behavior of single-element brace initialization: `json j{x}` is now equivalent to `json j(x)` (copy/move) rather than `json j = json::array({x})` (single-element array). Code relying on the old behavior should be updated to use `json::array({x})` explicitly. + To avoid any confusion and ensure portable code, **do not** use brace initialization with the types `basic_json`, `json`, or `ordered_json` unless you want to create an object or array as shown in the examples above. ## Limitations diff --git a/include/nlohmann/json.hpp b/include/nlohmann/json.hpp index aeeb33a0de..d4502a5c96 100644 --- a/include/nlohmann/json.hpp +++ b/include/nlohmann/json.hpp @@ -955,18 +955,6 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec } else { - // if there is exactly one element and type deduction is enabled, - // treat it as copy/move to avoid unintentionally wrapping a single - // json value in a single-element array - // (see https://github.com/nlohmann/json/issues/5074) - if (type_deduction && init.size() == 1) - { - *this = init.begin()->moved_or_copied(); - set_parents(); - assert_invariant(); - return; - } - // the initializer list describes an array -> create an array m_data.m_type = value_t::array; m_data.m_value.array = create(init.begin(), init.end()); diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 4eb892837b..ceb7a9f11d 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -3499,71 +3499,71 @@ NLOHMANN_JSON_NAMESPACE_END // SPDX-License-Identifier: MIT #ifndef INCLUDE_NLOHMANN_JSON_FWD_HPP_ -#define INCLUDE_NLOHMANN_JSON_FWD_HPP_ + #define INCLUDE_NLOHMANN_JSON_FWD_HPP_ -#include // int64_t, uint64_t -#include // map -#include // allocator -#include // string -#include // vector + #include // int64_t, uint64_t + #include // map + #include // allocator + #include // string + #include // vector -// #include + // #include -/*! -@brief namespace for Niels Lohmann -@see https://github.com/nlohmann -@since version 1.0.0 -*/ -NLOHMANN_JSON_NAMESPACE_BEGIN - -/*! -@brief default JSONSerializer template argument + /*! + @brief namespace for Niels Lohmann + @see https://github.com/nlohmann + @since version 1.0.0 + */ + NLOHMANN_JSON_NAMESPACE_BEGIN -This serializer ignores the template arguments and uses ADL -([argument-dependent lookup](https://en.cppreference.com/w/cpp/language/adl)) -for serialization. -*/ -template -struct adl_serializer; - -/// a class to store JSON values -/// @sa https://json.nlohmann.me/api/basic_json/ -template class ObjectType = - std::map, - template class ArrayType = std::vector, - class StringType = std::string, class BooleanType = bool, - class NumberIntegerType = std::int64_t, - class NumberUnsignedType = std::uint64_t, - class NumberFloatType = double, - template class AllocatorType = std::allocator, - template class JSONSerializer = - adl_serializer, - class BinaryType = std::vector, // cppcheck-suppress syntaxError - class CustomBaseClass = void> -class basic_json; + /*! + @brief default JSONSerializer template argument -/// @brief JSON Pointer defines a string syntax for identifying a specific value within a JSON document -/// @sa https://json.nlohmann.me/api/json_pointer/ -template -class json_pointer; + This serializer ignores the template arguments and uses ADL + ([argument-dependent lookup](https://en.cppreference.com/w/cpp/language/adl)) + for serialization. + */ + template + struct adl_serializer; + + /// a class to store JSON values + /// @sa https://json.nlohmann.me/api/basic_json/ + template class ObjectType = + std::map, + template class ArrayType = std::vector, + class StringType = std::string, class BooleanType = bool, + class NumberIntegerType = std::int64_t, + class NumberUnsignedType = std::uint64_t, + class NumberFloatType = double, + template class AllocatorType = std::allocator, + template class JSONSerializer = + adl_serializer, + class BinaryType = std::vector, // cppcheck-suppress syntaxError + class CustomBaseClass = void> + class basic_json; + + /// @brief JSON Pointer defines a string syntax for identifying a specific value within a JSON document + /// @sa https://json.nlohmann.me/api/json_pointer/ + template + class json_pointer; -/*! -@brief default specialization -@sa https://json.nlohmann.me/api/json/ -*/ -using json = basic_json<>; + /*! + @brief default specialization + @sa https://json.nlohmann.me/api/json/ + */ + using json = basic_json<>; -/// @brief a minimal map-like container that preserves insertion order -/// @sa https://json.nlohmann.me/api/ordered_map/ -template -struct ordered_map; + /// @brief a minimal map-like container that preserves insertion order + /// @sa https://json.nlohmann.me/api/ordered_map/ + template + struct ordered_map; -/// @brief specialization that maintains the insertion order of object keys -/// @sa https://json.nlohmann.me/api/ordered_json/ -using ordered_json = basic_json; + /// @brief specialization that maintains the insertion order of object keys + /// @sa https://json.nlohmann.me/api/ordered_json/ + using ordered_json = basic_json; -NLOHMANN_JSON_NAMESPACE_END + NLOHMANN_JSON_NAMESPACE_END #endif // INCLUDE_NLOHMANN_JSON_FWD_HPP_ @@ -5427,7 +5427,7 @@ NLOHMANN_JSON_NAMESPACE_END // #include - // JSON_HAS_CPP_17 +// JSON_HAS_CPP_17 #ifdef JSON_HAS_CPP_17 #include // optional #endif @@ -20255,10 +20255,10 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec const bool allow_exceptions = true, const bool ignore_comments = false, const bool ignore_trailing_commas = false - ) + ) { return ::nlohmann::detail::parser(std::move(adapter), - std::move(cb), allow_exceptions, ignore_comments, ignore_trailing_commas); + std::move(cb), allow_exceptions, ignore_comments, ignore_trailing_commas); } private: @@ -20956,8 +20956,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec detail::enable_if_t < !detail::is_basic_json::value && detail::is_compatible_type::value, int > = 0 > basic_json(CompatibleType && val) noexcept(noexcept( // NOLINT(bugprone-forwarding-reference-overload,bugprone-exception-escape) - JSONSerializer::to_json(std::declval(), - std::forward(val)))) + JSONSerializer::to_json(std::declval(), + std::forward(val)))) { JSONSerializer::to_json(*this, std::forward(val)); set_parents(); @@ -21074,18 +21074,6 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec } else { - // if there is exactly one element and type deduction is enabled, - // treat it as copy/move to avoid unintentionally wrapping a single - // json value in a single-element array - // (see https://github.com/nlohmann/json/issues/5074) - if (type_deduction && init.size() == 1) - { - *this = init.begin()->moved_or_copied(); - set_parents(); - assert_invariant(); - return; - } - // the initializer list describes an array -> create an array m_data.m_type = value_t::array; m_data.m_value.array = create(init.begin(), init.end()); @@ -21763,7 +21751,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec detail::has_from_json::value, int > = 0 > ValueType get_impl(detail::priority_tag<0> /*unused*/) const noexcept(noexcept( - JSONSerializer::from_json(std::declval(), std::declval()))) + JSONSerializer::from_json(std::declval(), std::declval()))) { auto ret = ValueType(); JSONSerializer::from_json(*this, ret); @@ -21805,7 +21793,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec detail::has_non_default_from_json::value, int > = 0 > ValueType get_impl(detail::priority_tag<1> /*unused*/) const noexcept(noexcept( - JSONSerializer::from_json(std::declval()))) + JSONSerializer::from_json(std::declval()))) { return JSONSerializer::from_json(*this); } @@ -21955,7 +21943,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec detail::has_from_json::value, int > = 0 > ValueType & get_to(ValueType& v) const noexcept(noexcept( - JSONSerializer::from_json(std::declval(), v))) + JSONSerializer::from_json(std::declval(), v))) { JSONSerializer::from_json(*this, v); return v; diff --git a/tests/src/unit-class_parser.cpp b/tests/src/unit-class_parser.cpp index e42ac17cd8..f41d8efa0c 100644 --- a/tests/src/unit-class_parser.cpp +++ b/tests/src/unit-class_parser.cpp @@ -1506,7 +1506,7 @@ TEST_CASE("parser class") // removed all objects in array. CHECK (j_filtered2.size() == 1); - CHECK (j_filtered2 == json::array({1})); + CHECK (j_filtered2 == json({1})); } SECTION("filter specific events") diff --git a/tests/src/unit-class_parser_diagnostic_positions.cpp b/tests/src/unit-class_parser_diagnostic_positions.cpp index ed043d3e24..f09ec0f6f4 100644 --- a/tests/src/unit-class_parser_diagnostic_positions.cpp +++ b/tests/src/unit-class_parser_diagnostic_positions.cpp @@ -1517,7 +1517,7 @@ TEST_CASE("parser class") // removed all objects in array. CHECK (j_filtered2.size() == 1); - CHECK (j_filtered2 == json::array({1})); + CHECK (j_filtered2 == json({1})); } SECTION("filter specific events") diff --git a/tests/src/unit-constructor1.cpp b/tests/src/unit-constructor1.cpp index 098c0ab425..8eb7d6ab55 100644 --- a/tests/src/unit-constructor1.cpp +++ b/tests/src/unit-constructor1.cpp @@ -283,8 +283,7 @@ TEST_CASE("constructors") SECTION("std::pair/tuple/array failures") { - // Use json::array() to explicitly create a single-element array - json const j = json::array({1}); + json const j{1}; CHECK_THROWS_WITH_AS((j.get>()), "[json.exception.out_of_range.401] array index 1 is out of range", json::out_of_range&); CHECK_THROWS_WITH_AS((j.get>()), "[json.exception.out_of_range.401] array index 1 is out of range", json::out_of_range&); @@ -936,18 +935,14 @@ TEST_CASE("constructors") { SECTION("explicit") { - // j is a copy of the empty array element, not [[]] json const j(json::initializer_list_t {json(json::array_t())}); CHECK(j.type() == json::value_t::array); - CHECK(j.empty()); } SECTION("implicit") { - // j is a copy of the empty array element, not [[]] json const j {json::array_t()}; CHECK(j.type() == json::value_t::array); - CHECK(j.empty()); } } @@ -956,13 +951,13 @@ TEST_CASE("constructors") SECTION("explicit") { json const j(json::initializer_list_t {json(json::object_t())}); - CHECK(j.type() == json::value_t::object); + CHECK(j.type() == json::value_t::array); } SECTION("implicit") { json const j {json::object_t()}; - CHECK(j.type() == json::value_t::object); + CHECK(j.type() == json::value_t::array); } } @@ -971,13 +966,13 @@ TEST_CASE("constructors") SECTION("explicit") { json const j(json::initializer_list_t {json("Hello world")}); - CHECK(j.type() == json::value_t::string); + CHECK(j.type() == json::value_t::array); } SECTION("implicit") { json const j {"Hello world"}; - CHECK(j.type() == json::value_t::string); + CHECK(j.type() == json::value_t::array); } } @@ -986,13 +981,13 @@ TEST_CASE("constructors") SECTION("explicit") { json const j(json::initializer_list_t {json(true)}); - CHECK(j.type() == json::value_t::boolean); + CHECK(j.type() == json::value_t::array); } SECTION("implicit") { json const j {true}; - CHECK(j.type() == json::value_t::boolean); + CHECK(j.type() == json::value_t::array); } } @@ -1001,13 +996,13 @@ TEST_CASE("constructors") SECTION("explicit") { json const j(json::initializer_list_t {json(1)}); - CHECK(j.type() == json::value_t::number_integer); + CHECK(j.type() == json::value_t::array); } SECTION("implicit") { json const j {1}; - CHECK(j.type() == json::value_t::number_integer); + CHECK(j.type() == json::value_t::array); } } @@ -1016,13 +1011,13 @@ TEST_CASE("constructors") SECTION("explicit") { json const j(json::initializer_list_t {json(1u)}); - CHECK(j.type() == json::value_t::number_unsigned); + CHECK(j.type() == json::value_t::array); } SECTION("implicit") { json const j {1u}; - CHECK(j.type() == json::value_t::number_unsigned); + CHECK(j.type() == json::value_t::array); } } @@ -1031,13 +1026,13 @@ TEST_CASE("constructors") SECTION("explicit") { json const j(json::initializer_list_t {json(42.23)}); - CHECK(j.type() == json::value_t::number_float); + CHECK(j.type() == json::value_t::array); } SECTION("implicit") { json const j {42.23}; - CHECK(j.type() == json::value_t::number_float); + CHECK(j.type() == json::value_t::array); } } } @@ -1115,7 +1110,7 @@ TEST_CASE("constructors") std::string source(1024, '!'); const auto* source_addr = source.data(); json j = {std::move(source)}; - const auto* target_addr = j.get_ref().data(); + const auto* target_addr = j[0].get_ref().data(); const bool success = (target_addr == source_addr); CHECK(success); } @@ -1150,7 +1145,7 @@ TEST_CASE("constructors") json::array_t source = {1, 2, 3}; const auto* source_addr = source.data(); json j {std::move(source)}; - const auto* target_addr = j.get_ref().data(); + const auto* target_addr = j[0].get_ref().data(); const bool success = (target_addr == source_addr); CHECK(success); } @@ -1170,7 +1165,7 @@ TEST_CASE("constructors") json::array_t source = {1, 2, 3}; const auto* source_addr = source.data(); json j = {std::move(source)}; - const auto* target_addr = j.get_ref().data(); + const auto* target_addr = j[0].get_ref().data(); const bool success = (target_addr == source_addr); CHECK(success); } @@ -1193,7 +1188,7 @@ TEST_CASE("constructors") json::object_t source = {{"hello", "world"}}; const json* source_addr = &source.at("hello"); json j {std::move(source)}; - CHECK(&(j.get_ref().at("hello")) == source_addr); + CHECK(&(j[0].get_ref().at("hello")) == source_addr); } SECTION("constructor with implicit types (object)") @@ -1209,7 +1204,7 @@ TEST_CASE("constructors") json::object_t source = {{"hello", "world"}}; const json* source_addr = &source.at("hello"); json j = {std::move(source)}; - CHECK(&(j.get_ref().at("hello")) == source_addr); + CHECK(&(j[0].get_ref().at("hello")) == source_addr); } SECTION("assignment with implicit types (object)") diff --git a/tests/src/unit-modifiers.cpp b/tests/src/unit-modifiers.cpp index 9821b756a6..bf9bd83503 100644 --- a/tests/src/unit-modifiers.cpp +++ b/tests/src/unit-modifiers.cpp @@ -310,7 +310,7 @@ TEST_CASE("modifiers") auto& x = j.emplace_back(3, "foo"); CHECK(x == json({"foo", "foo", "foo"})); CHECK(j.type() == json::value_t::array); - CHECK(j == json::array({json({"foo", "foo", "foo"})})); + CHECK(j == json({{"foo", "foo", "foo"}})); } } diff --git a/tests/src/unit-regression2.cpp b/tests/src/unit-regression2.cpp index 36747489b7..5ef7da59d4 100644 --- a/tests/src/unit-regression2.cpp +++ b/tests/src/unit-regression2.cpp @@ -844,7 +844,7 @@ TEST_CASE("regression tests 2") SECTION("std::tuple") { { - const json j = json::array({9}); + const json j = {9}; auto t = j.get>(); CHECK(std::get<0>(t).x == 9); } @@ -1119,57 +1119,6 @@ TEST_CASE("regression tests 2") CHECK((decoded == json_4804::array())); } #endif - - SECTION("copy constructor changes semantics with brace initialization") - { - // object - { - json const j_obj = {{"key", "value"}, {"num", 42}}; - json const j{j_obj}; - CHECK(j.is_object()); - CHECK(j == j_obj); - } - - // array - { - json const j_arr = {1, 2, 3}; - json const j{j_arr}; - CHECK(j.is_array()); - CHECK(j == j_arr); - } - - // primitives - { - json const j_bool{true}; - CHECK(j_bool.is_boolean()); - - json const j_num{42}; - CHECK(j_num.is_number_integer()); - - json const j_float{3.14}; - CHECK(j_float.is_number_float()); - - json const j_str{"hello"}; - CHECK(j_str.is_string()); - } - - // passing by-value should still work correctly - { - auto receive_by_value = [](json j) { return j; }; - json const j_obj = {{"key", "value"}}; - json const result = receive_by_value(j_obj); - CHECK(result.is_object()); - CHECK(result == j_obj); - } - - // explicitly creating a single-element array still works via json::array() - { - json const j_arr = json::array({42}); - CHECK(j_arr.is_array()); - CHECK(j_arr.size() == 1); - CHECK(j_arr[0] == 42); - } - } } TEST_CASE_TEMPLATE("issue #4798 - nlohmann::json::to_msgpack() encode float NaN as double", T, double, float) // NOLINT(readability-math-missing-parentheses, bugprone-throwing-static-initialization) From 251269f35a498e577ecea46eae345065ff3be423 Mon Sep 17 00:00:00 2001 From: Samaresh Kumar Singh Date: Wed, 1 Apr 2026 20:23:29 -0500 Subject: [PATCH 4/9] Fix: add JSON_BRACE_INIT_COPY_SEMANTICS opt-in macro for issue #5074 Single-element brace initialization wrapping in an array cannot be fixed without breaking existing code. Added JSON_BRACE_INIT_COPY_SEMANTICS as an opt-in macro (default 0) so users can enable copy/move semantics for single-element brace init without affecting anyone relying on the current behavior. Signed-off-by: Samaresh Kumar Singh --- docs/mkdocs/docs/home/faq.md | 20 ++++++++++ include/nlohmann/detail/macro_scope.hpp | 4 ++ include/nlohmann/detail/macro_unscope.hpp | 1 + include/nlohmann/json.hpp | 9 +++++ single_include/nlohmann/json.hpp | 14 +++++++ tests/src/unit-regression2.cpp | 48 +++++++++++++++++++++++ 6 files changed, 96 insertions(+) diff --git a/docs/mkdocs/docs/home/faq.md b/docs/mkdocs/docs/home/faq.md index 74709bac68..92cc935810 100644 --- a/docs/mkdocs/docs/home/faq.md +++ b/docs/mkdocs/docs/home/faq.md @@ -38,6 +38,26 @@ for objects. To avoid any confusion and ensure portable code, **do not** use brace initialization with the types `basic_json`, `json`, or `ordered_json` unless you want to create an object or array as shown in the examples above. + To explicitly create a single-element array, use `json::array({value})`: + + ```cpp + json j = json::array({true}); // [true] + ``` + +**Opt-in copy semantics (since version 3.12.0)** + +If you define `JSON_BRACE_INIT_COPY_SEMANTICS` to `1` before including the library, single-element brace initialization is treated as copy/move instead of creating a single-element array: + +```cpp +#define JSON_BRACE_INIT_COPY_SEMANTICS 1 +#include + +json obj = {{"key", "value"}}; +json j{obj}; // -> {"key":"value"} (copy, not array) +``` + +Without the macro (default behavior), `json j{obj}` creates `[{"key":"value"}]`. This opt-in macro fixes issue #5074 while preserving backwards compatibility for existing code. + ## Limitations ### Relaxed parsing diff --git a/include/nlohmann/detail/macro_scope.hpp b/include/nlohmann/detail/macro_scope.hpp index 833b7ddf04..3904b66371 100644 --- a/include/nlohmann/detail/macro_scope.hpp +++ b/include/nlohmann/detail/macro_scope.hpp @@ -599,3 +599,7 @@ #ifndef JSON_USE_GLOBAL_UDLS #define JSON_USE_GLOBAL_UDLS 1 #endif + +#ifndef JSON_BRACE_INIT_COPY_SEMANTICS + #define JSON_BRACE_INIT_COPY_SEMANTICS 0 +#endif diff --git a/include/nlohmann/detail/macro_unscope.hpp b/include/nlohmann/detail/macro_unscope.hpp index 975d9f0d2d..60ed623163 100644 --- a/include/nlohmann/detail/macro_unscope.hpp +++ b/include/nlohmann/detail/macro_unscope.hpp @@ -26,6 +26,7 @@ #undef JSON_NO_UNIQUE_ADDRESS #undef JSON_DISABLE_ENUM_SERIALIZATION #undef JSON_USE_GLOBAL_UDLS +#undef JSON_BRACE_INIT_COPY_SEMANTICS #ifndef JSON_TEST_KEEP_MACROS #undef JSON_CATCH diff --git a/include/nlohmann/json.hpp b/include/nlohmann/json.hpp index d4502a5c96..92fbd572c6 100644 --- a/include/nlohmann/json.hpp +++ b/include/nlohmann/json.hpp @@ -955,6 +955,15 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec } else { +#if JSON_BRACE_INIT_COPY_SEMANTICS + if (type_deduction && init.size() == 1) + { + *this = init.begin()->moved_or_copied(); + set_parents(); + assert_invariant(); + return; + } +#endif // the initializer list describes an array -> create an array m_data.m_type = value_t::array; m_data.m_value.array = create(init.begin(), init.end()); diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index ceb7a9f11d..c9008fe8c1 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -2964,6 +2964,10 @@ JSON_HEDLEY_DIAGNOSTIC_POP #define JSON_USE_GLOBAL_UDLS 1 #endif +#ifndef JSON_BRACE_INIT_COPY_SEMANTICS + #define JSON_BRACE_INIT_COPY_SEMANTICS 0 +#endif + #if JSON_HAS_THREE_WAY_COMPARISON #include // partial_ordering #endif @@ -21074,6 +21078,15 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec } else { +#if JSON_BRACE_INIT_COPY_SEMANTICS + if (type_deduction && init.size() == 1) + { + *this = init.begin()->moved_or_copied(); + set_parents(); + assert_invariant(); + return; + } +#endif // the initializer list describes an array -> create an array m_data.m_type = value_t::array; m_data.m_value.array = create(init.begin(), init.end()); @@ -25529,6 +25542,7 @@ inline void swap(nlohmann::NLOHMANN_BASIC_JSON_TPL& j1, nlohmann::NLOHMANN_BASIC #undef JSON_NO_UNIQUE_ADDRESS #undef JSON_DISABLE_ENUM_SERIALIZATION #undef JSON_USE_GLOBAL_UDLS +#undef JSON_BRACE_INIT_COPY_SEMANTICS #ifndef JSON_TEST_KEEP_MACROS #undef JSON_CATCH diff --git a/tests/src/unit-regression2.cpp b/tests/src/unit-regression2.cpp index 5ef7da59d4..46eeca1d4d 100644 --- a/tests/src/unit-regression2.cpp +++ b/tests/src/unit-regression2.cpp @@ -1188,4 +1188,52 @@ TEST_CASE_TEMPLATE("issue #4798 - nlohmann::json::to_msgpack() encode float NaN CHECK(json::from_cbor(cbor_z_3).get() == -std::numeric_limits::infinity()); } +TEST_CASE("regression test #5074 - single-element brace init default behavior") +{ + json const j_obj = {{"key", "value"}}; + json const j_arr = {1, 2, 3}; + + json const j1{j_obj}; + CHECK(j1.is_array()); + CHECK(j1.size() == 1); + CHECK(j1[0] == j_obj); + + json const j2{j_arr}; + CHECK(j2.is_array()); + CHECK(j2.size() == 1); + CHECK(j2[0] == j_arr); + + json const j3 = json::array({j_obj}); + CHECK(j3.is_array()); + CHECK(j3.size() == 1); + CHECK(j3[0] == j_obj); +} + +#if JSON_BRACE_INIT_COPY_SEMANTICS +TEST_CASE("regression test #5074 - single-element brace init with JSON_BRACE_INIT_COPY_SEMANTICS") +{ + // with JSON_BRACE_INIT_COPY_SEMANTICS: single-element brace init copies/moves + json const j_obj = {{"key", "value"}, {"num", 42}}; + json const j_arr = {1, 2, 3}; + + // object: brace init copies instead of wrapping + json const j1{j_obj}; + CHECK(j1.is_object()); + CHECK(j1 == j_obj); + + // array: brace init copies instead of wrapping + json const j2{j_arr}; + CHECK(j2.is_array()); + CHECK(j2.size() == 3); + CHECK(j2 == j_arr); + + // primitives still work as initializer lists + json const j3{true}; + CHECK(j3.is_boolean()); + + json const j4{42}; + CHECK(j4.is_number_integer()); +} +#endif + DOCTEST_CLANG_SUPPRESS_WARNING_POP From 20da46e52cb3dcf3d6cbc9c06ce52f6a56428011 Mon Sep 17 00:00:00 2001 From: Samaresh Kumar Singh Date: Tue, 7 Apr 2026 17:15:03 -0500 Subject: [PATCH 5/9] docs: add dedicated macro page and CI test target for JSON_BRACE_INIT_COPY_SEMANTICS Signed-off-by: Samaresh Kumar Singh --- cmake/ci.cmake | 15 +++ docs/mkdocs/docs/api/macros/index.md | 1 + .../macros/json_brace_init_copy_semantics.md | 93 +++++++++++++++++++ 3 files changed, 109 insertions(+) create mode 100644 docs/mkdocs/docs/api/macros/json_brace_init_copy_semantics.md diff --git a/cmake/ci.cmake b/cmake/ci.cmake index f711368649..559ce8f55d 100644 --- a/cmake/ci.cmake +++ b/cmake/ci.cmake @@ -212,6 +212,21 @@ add_custom_target(ci_test_legacycomparison COMMENT "Compile and test with legacy discarded value comparison enabled" ) +############################################################################### +# Enable brace-init copy semantics. +############################################################################### + +add_custom_target(ci_test_brace_init_copy_semantics + COMMAND ${CMAKE_COMMAND} + -DCMAKE_BUILD_TYPE=Debug -GNinja + -DJSON_BuildTests=ON -DJSON_FastTests=ON + -DCMAKE_CXX_FLAGS=-DJSON_BRACE_INIT_COPY_SEMANTICS=1 + -S${PROJECT_SOURCE_DIR} -B${PROJECT_BINARY_DIR}/build_brace_init_copy_semantics + COMMAND ${CMAKE_COMMAND} --build ${PROJECT_BINARY_DIR}/build_brace_init_copy_semantics + COMMAND cd ${PROJECT_BINARY_DIR}/build_brace_init_copy_semantics && ${CMAKE_CTEST_COMMAND} --parallel ${N} --output-on-failure + COMMENT "Compile and test with brace-init copy semantics enabled" +) + ############################################################################### # Disable global UDLs. ############################################################################### diff --git a/docs/mkdocs/docs/api/macros/index.md b/docs/mkdocs/docs/api/macros/index.md index 59e4b3d280..cfe801d43e 100644 --- a/docs/mkdocs/docs/api/macros/index.md +++ b/docs/mkdocs/docs/api/macros/index.md @@ -40,6 +40,7 @@ header. See also the [macro overview page](../../features/macros.md). ## Type conversions +- [**JSON_BRACE_INIT_COPY_SEMANTICS**](json_brace_init_copy_semantics.md) - opt in to copy/move semantics for single-element brace initialization - [**JSON_DISABLE_ENUM_SERIALIZATION**](json_disable_enum_serialization.md) - switch off default serialization/deserialization functions for enums - [**JSON_USE_IMPLICIT_CONVERSIONS**](json_use_implicit_conversions.md) - control implicit conversions 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 new file mode 100644 index 0000000000..3942f93582 --- /dev/null +++ b/docs/mkdocs/docs/api/macros/json_brace_init_copy_semantics.md @@ -0,0 +1,93 @@ +# JSON_BRACE_INIT_COPY_SEMANTICS + +```cpp +#define JSON_BRACE_INIT_COPY_SEMANTICS /* value */ +``` + +When defined to `1`, single-element brace initialization of a `basic_json` value is treated as a copy/move of the +element rather than wrapping it in a single-element array. + +## Background + +C++ always prefers the `initializer_list` constructor over the copy/move constructor for brace initialization. This +means that code like + +```cpp +json obj = {{"key", "value"}}; +json j{obj}; +``` + +creates a single-element **array** `[{"key":"value"}]` instead of a copy of `obj`. This behavior is compiler-dependent +for older compilers (GCC wrapped, Clang did not), but starting from Clang 20, both compilers behave the same way. + +Enabling this macro opts into copy/move semantics for this case (see [#5074](https://github.com/nlohmann/json/issues/5074)). + +## Default definition + +The default value is `0` (disabled — existing behavior is preserved). + +```cpp +#define JSON_BRACE_INIT_COPY_SEMANTICS 0 +``` + +## Notes + +!!! warning "Opt-in only" + + This macro must be defined **before** including ``. Defining it after the include has no effect. + +!!! tip "Workaround without the macro" + + To explicitly create a single-element array without enabling this macro, use `json::array()`: + + ```cpp + json j = json::array({obj}); // always creates [obj] + ``` + +## Examples + +??? example "Default behavior (macro not defined)" + + Without the macro, single-element brace initialization wraps the value in an array: + + ```cpp + #include + + using json = nlohmann::json; + + int main() + { + json obj = {{"key", "value"}}; + + json j{obj}; + // j is [{"key":"value"}] -- single-element array, NOT a copy of obj + } + ``` + +??? example "Opt-in copy semantics (macro defined to 1)" + + With the macro, single-element brace initialization copies/moves the value: + + ```cpp + #define JSON_BRACE_INIT_COPY_SEMANTICS 1 + #include + + using json = nlohmann::json; + + int main() + { + json obj = {{"key", "value"}}; + + json j{obj}; + // j is {"key":"value"} -- copy of obj + } + ``` + +## See also + +- [FAQ: Brace initialization yields arrays](../../home/faq.md#brace-initialization-yields-arrays) +- [**basic_json(initializer_list_t)**](../basic_json/basic_json.md) - the affected constructor + +## Version history + +- Added in version 3.12.0. From ad9880ddf5e90724e444a3cf756e01c4e2e6e170 Mon Sep 17 00:00:00 2001 From: Samaresh Kumar Singh Date: Tue, 7 Apr 2026 18:16:35 -0500 Subject: [PATCH 6/9] fix: remove compiler-dependent assertions from #5074 regression test Signed-off-by: Samaresh Kumar Singh --- tests/src/unit-regression2.cpp | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/tests/src/unit-regression2.cpp b/tests/src/unit-regression2.cpp index 46eeca1d4d..5d512c3ac5 100644 --- a/tests/src/unit-regression2.cpp +++ b/tests/src/unit-regression2.cpp @@ -1188,25 +1188,14 @@ TEST_CASE_TEMPLATE("issue #4798 - nlohmann::json::to_msgpack() encode float NaN CHECK(json::from_cbor(cbor_z_3).get() == -std::numeric_limits::infinity()); } -TEST_CASE("regression test #5074 - single-element brace init default behavior") +TEST_CASE("regression test #5074 - portable workaround for single-element brace init") { json const j_obj = {{"key", "value"}}; - json const j_arr = {1, 2, 3}; - - json const j1{j_obj}; - CHECK(j1.is_array()); - CHECK(j1.size() == 1); - CHECK(j1[0] == j_obj); - - json const j2{j_arr}; - CHECK(j2.is_array()); - CHECK(j2.size() == 1); - CHECK(j2[0] == j_arr); - json const j3 = json::array({j_obj}); - CHECK(j3.is_array()); - CHECK(j3.size() == 1); - CHECK(j3[0] == j_obj); + json const j = json::array({j_obj}); + CHECK(j.is_array()); + CHECK(j.size() == 1); + CHECK(j[0] == j_obj); } #if JSON_BRACE_INIT_COPY_SEMANTICS From 5fa9548bb67f966a452061b4c25a7da6048f9156 Mon Sep 17 00:00:00 2001 From: Samaresh Kumar Singh Date: Tue, 7 Apr 2026 18:42:30 -0500 Subject: [PATCH 7/9] fix: use defined() guard for JSON_BRACE_INIT_COPY_SEMANTICS to satisfy -Wundef Signed-off-by: Samaresh Kumar Singh --- tests/src/unit-regression2.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/src/unit-regression2.cpp b/tests/src/unit-regression2.cpp index 5d512c3ac5..9a42b765fb 100644 --- a/tests/src/unit-regression2.cpp +++ b/tests/src/unit-regression2.cpp @@ -1198,7 +1198,7 @@ TEST_CASE("regression test #5074 - portable workaround for single-element brace CHECK(j[0] == j_obj); } -#if JSON_BRACE_INIT_COPY_SEMANTICS +#if defined(JSON_BRACE_INIT_COPY_SEMANTICS) && (JSON_BRACE_INIT_COPY_SEMANTICS == 1) TEST_CASE("regression test #5074 - single-element brace init with JSON_BRACE_INIT_COPY_SEMANTICS") { // with JSON_BRACE_INIT_COPY_SEMANTICS: single-element brace init copies/moves From d2757325e66f592c1bea07c8a00bf7e1c1a7b1cc Mon Sep 17 00:00:00 2001 From: Samaresh Kumar Singh Date: Tue, 7 Apr 2026 19:15:42 -0500 Subject: [PATCH 8/9] docs: fix section name in json_brace_init_copy_semantics.md to pass style check Signed-off-by: Samaresh Kumar Singh --- .../macros/json_brace_init_copy_semantics.md | 40 ++++++++++--------- 1 file changed, 21 insertions(+), 19 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 3942f93582..6dfb910f1b 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 @@ -7,30 +7,24 @@ When defined to `1`, single-element brace initialization of a `basic_json` value is treated as a copy/move of the element rather than wrapping it in a single-element array. -## Background - -C++ always prefers the `initializer_list` constructor over the copy/move constructor for brace initialization. This -means that code like - -```cpp -json obj = {{"key", "value"}}; -json j{obj}; -``` +## Notes -creates a single-element **array** `[{"key":"value"}]` instead of a copy of `obj`. This behavior is compiler-dependent -for older compilers (GCC wrapped, Clang did not), but starting from Clang 20, both compilers behave the same way. +!!! note "Background" -Enabling this macro opts into copy/move semantics for this case (see [#5074](https://github.com/nlohmann/json/issues/5074)). + C++ always prefers the `initializer_list` constructor over the copy/move constructor for brace initialization. This + means that code like -## Default definition - -The default value is `0` (disabled — existing behavior is preserved). + ```cpp + json obj = {{"key", "value"}}; + json j{obj}; + ``` -```cpp -#define JSON_BRACE_INIT_COPY_SEMANTICS 0 -``` + creates a single-element **array** `[{"key":"value"}]` instead of a copy of `obj`. This behavior is + compiler-dependent for older compilers (GCC wrapped, Clang did not), but starting from Clang 20, both compilers + behave the same way. -## Notes + Enabling this macro opts into copy/move semantics for this case + (see [#5074](https://github.com/nlohmann/json/issues/5074)). !!! warning "Opt-in only" @@ -44,6 +38,14 @@ The default value is `0` (disabled — existing behavior is preserved). json j = json::array({obj}); // always creates [obj] ``` +## Default definition + +The default value is `0` (disabled — existing behavior is preserved). + +```cpp +#define JSON_BRACE_INIT_COPY_SEMANTICS 0 +``` + ## Examples ??? example "Default behavior (macro not defined)" From 214e4cf485b6dc9f9e00cb90f94e008e757de94d Mon Sep 17 00:00:00 2001 From: Samaresh Kumar Singh Date: Tue, 7 Apr 2026 19:19:32 -0500 Subject: [PATCH 9/9] docs: move Default definition section before Notes to fix style check order Signed-off-by: Samaresh Kumar Singh --- .../api/macros/json_brace_init_copy_semantics.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 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 6dfb910f1b..8d88acf473 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 @@ -7,6 +7,14 @@ When defined to `1`, single-element brace initialization of a `basic_json` value is treated as a copy/move of the element rather than wrapping it in a single-element array. +## Default definition + +The default value is `0` (disabled — existing behavior is preserved). + +```cpp +#define JSON_BRACE_INIT_COPY_SEMANTICS 0 +``` + ## Notes !!! note "Background" @@ -38,14 +46,6 @@ element rather than wrapping it in a single-element array. json j = json::array({obj}); // always creates [obj] ``` -## Default definition - -The default value is `0` (disabled — existing behavior is preserved). - -```cpp -#define JSON_BRACE_INIT_COPY_SEMANTICS 0 -``` - ## Examples ??? example "Default behavior (macro not defined)"