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/detail/conversions/from_json.hpp b/include/nlohmann/detail/conversions/from_json.hpp index 34270e2368..eb3344a654 100644 --- a/include/nlohmann/detail/conversions/from_json.hpp +++ b/include/nlohmann/detail/conversions/from_json.hpp @@ -386,14 +386,10 @@ inline void from_json(const BasicJsonType& j, ConstructibleObjectType& obj) ConstructibleObjectType ret; const auto* inner_object = j.template get_ptr(); - using value_type = typename ConstructibleObjectType::value_type; - std::transform( - inner_object->begin(), inner_object->end(), - std::inserter(ret, ret.begin()), - [](typename BasicJsonType::object_t::value_type const & p) + for (const auto& p : *inner_object) { - return value_type(p.first, p.second.template get()); - }); + ret.emplace(p.first, p.second.template get()); + } obj = std::move(ret); } 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..75feb8d531 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 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_ @@ -5219,14 +5219,10 @@ inline void from_json(const BasicJsonType& j, ConstructibleObjectType& obj) ConstructibleObjectType ret; const auto* inner_object = j.template get_ptr(); - using value_type = typename ConstructibleObjectType::value_type; - std::transform( - inner_object->begin(), inner_object->end(), - std::inserter(ret, ret.begin()), - [](typename BasicJsonType::object_t::value_type const & p) + for (const auto& p : *inner_object) { - return value_type(p.first, p.second.template get()); - }); + ret.emplace(p.first, p.second.template get()); + } obj = std::move(ret); } @@ -5427,7 +5423,7 @@ NLOHMANN_JSON_NAMESPACE_END // #include -// JSON_HAS_CPP_17 + // JSON_HAS_CPP_17 #ifdef JSON_HAS_CPP_17 #include // optional #endif @@ -20255,10 +20251,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 +20952,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 +21070,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()); @@ -21751,7 +21759,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); @@ -21793,7 +21801,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); } @@ -21943,7 +21951,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 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)