Skip to content

Commit bc6bfc3

Browse files
committed
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
1 parent 49026f7 commit bc6bfc3

File tree

8 files changed

+113
-26
lines changed

8 files changed

+113
-26
lines changed

docs/mkdocs/docs/home/faq.md

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,16 @@
2020

2121
yield different results (`#!json [true]` vs. `#!json true`)?
2222

23-
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
23+
Starting from this version, single-value brace initialization is treated as copy/move instead of wrapping in a single-element array:
24+
25+
```cpp
26+
json j_orig = {{"key", "value"}};
27+
28+
json j{j_orig};
29+
j_orig.method();
30+
```
31+
32+
The root cause was the library's constructor overloads for initializer lists, which allow syntax like
2433
2534
```cpp
2635
json array = {1, 2, 3, 4};
@@ -32,11 +41,13 @@ for arrays and
3241
json object = {{"one", 1}, {"two", 2}};
3342
```
3443
35-
for objects.
44+
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.
3645
37-
!!! tip
46+
After the fix, a single-element brace-init behaves like copy/move initialization. To explicitly create a single-element array, use `json::array({value})`.
47+
48+
!!! note
3849
39-
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.
50+
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.
4051
4152
## Limitations
4253

include/nlohmann/json.hpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -955,6 +955,18 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
955955
}
956956
else
957957
{
958+
// if there is exactly one element and type deduction is enabled,
959+
// treat it as copy/move to avoid unintentionally wrapping a single
960+
// json value in a single-element array
961+
// (see https://github.com/nlohmann/json/issues/5074)
962+
if (type_deduction && init.size() == 1)
963+
{
964+
*this = init.begin()->moved_or_copied();
965+
set_parents();
966+
assert_invariant();
967+
return;
968+
}
969+
958970
// the initializer list describes an array -> create an array
959971
m_data.m_type = value_t::array;
960972
m_data.m_value.array = create<array_t>(init.begin(), init.end());

single_include/nlohmann/json.hpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21074,6 +21074,14 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
2107421074
}
2107521075
else
2107621076
{
21077+
if (type_deduction && init.size() == 1)
21078+
{
21079+
*this = init.begin()->moved_or_copied();
21080+
set_parents();
21081+
assert_invariant();
21082+
return;
21083+
}
21084+
2107721085
// the initializer list describes an array -> create an array
2107821086
m_data.m_type = value_t::array;
2107921087
m_data.m_value.array = create<array_t>(init.begin(), init.end());

tests/src/unit-class_parser.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1506,7 +1506,7 @@ TEST_CASE("parser class")
15061506

15071507
// removed all objects in array.
15081508
CHECK (j_filtered2.size() == 1);
1509-
CHECK (j_filtered2 == json({1}));
1509+
CHECK (j_filtered2 == json::array({1}));
15101510
}
15111511

15121512
SECTION("filter specific events")

tests/src/unit-class_parser_diagnostic_positions.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1517,7 +1517,7 @@ TEST_CASE("parser class")
15171517

15181518
// removed all objects in array.
15191519
CHECK (j_filtered2.size() == 1);
1520-
CHECK (j_filtered2 == json({1}));
1520+
CHECK (j_filtered2 == json::array({1}));
15211521
}
15221522

15231523
SECTION("filter specific events")

tests/src/unit-constructor1.cpp

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,8 @@ TEST_CASE("constructors")
283283

284284
SECTION("std::pair/tuple/array failures")
285285
{
286-
json const j{1};
286+
// Use json::array() to explicitly create a single-element array
287+
json const j = json::array({1});
287288

288289
CHECK_THROWS_WITH_AS((j.get<std::pair<int, int>>()), "[json.exception.out_of_range.401] array index 1 is out of range", json::out_of_range&);
289290
CHECK_THROWS_WITH_AS((j.get<std::tuple<int, int>>()), "[json.exception.out_of_range.401] array index 1 is out of range", json::out_of_range&);
@@ -935,14 +936,18 @@ TEST_CASE("constructors")
935936
{
936937
SECTION("explicit")
937938
{
939+
// j is a copy of the empty array element, not [[]]
938940
json const j(json::initializer_list_t {json(json::array_t())});
939941
CHECK(j.type() == json::value_t::array);
942+
CHECK(j.empty());
940943
}
941944

942945
SECTION("implicit")
943946
{
947+
// j is a copy of the empty array element, not [[]]
944948
json const j {json::array_t()};
945949
CHECK(j.type() == json::value_t::array);
950+
CHECK(j.empty());
946951
}
947952
}
948953

@@ -951,13 +956,13 @@ TEST_CASE("constructors")
951956
SECTION("explicit")
952957
{
953958
json const j(json::initializer_list_t {json(json::object_t())});
954-
CHECK(j.type() == json::value_t::array);
959+
CHECK(j.type() == json::value_t::object);
955960
}
956961

957962
SECTION("implicit")
958963
{
959964
json const j {json::object_t()};
960-
CHECK(j.type() == json::value_t::array);
965+
CHECK(j.type() == json::value_t::object);
961966
}
962967
}
963968

@@ -966,13 +971,13 @@ TEST_CASE("constructors")
966971
SECTION("explicit")
967972
{
968973
json const j(json::initializer_list_t {json("Hello world")});
969-
CHECK(j.type() == json::value_t::array);
974+
CHECK(j.type() == json::value_t::string);
970975
}
971976

972977
SECTION("implicit")
973978
{
974979
json const j {"Hello world"};
975-
CHECK(j.type() == json::value_t::array);
980+
CHECK(j.type() == json::value_t::string);
976981
}
977982
}
978983

@@ -981,13 +986,13 @@ TEST_CASE("constructors")
981986
SECTION("explicit")
982987
{
983988
json const j(json::initializer_list_t {json(true)});
984-
CHECK(j.type() == json::value_t::array);
989+
CHECK(j.type() == json::value_t::boolean);
985990
}
986991

987992
SECTION("implicit")
988993
{
989994
json const j {true};
990-
CHECK(j.type() == json::value_t::array);
995+
CHECK(j.type() == json::value_t::boolean);
991996
}
992997
}
993998

@@ -996,13 +1001,13 @@ TEST_CASE("constructors")
9961001
SECTION("explicit")
9971002
{
9981003
json const j(json::initializer_list_t {json(1)});
999-
CHECK(j.type() == json::value_t::array);
1004+
CHECK(j.type() == json::value_t::number_integer);
10001005
}
10011006

10021007
SECTION("implicit")
10031008
{
10041009
json const j {1};
1005-
CHECK(j.type() == json::value_t::array);
1010+
CHECK(j.type() == json::value_t::number_integer);
10061011
}
10071012
}
10081013

@@ -1011,13 +1016,13 @@ TEST_CASE("constructors")
10111016
SECTION("explicit")
10121017
{
10131018
json const j(json::initializer_list_t {json(1u)});
1014-
CHECK(j.type() == json::value_t::array);
1019+
CHECK(j.type() == json::value_t::number_unsigned);
10151020
}
10161021

10171022
SECTION("implicit")
10181023
{
10191024
json const j {1u};
1020-
CHECK(j.type() == json::value_t::array);
1025+
CHECK(j.type() == json::value_t::number_unsigned);
10211026
}
10221027
}
10231028

@@ -1026,13 +1031,13 @@ TEST_CASE("constructors")
10261031
SECTION("explicit")
10271032
{
10281033
json const j(json::initializer_list_t {json(42.23)});
1029-
CHECK(j.type() == json::value_t::array);
1034+
CHECK(j.type() == json::value_t::number_float);
10301035
}
10311036

10321037
SECTION("implicit")
10331038
{
10341039
json const j {42.23};
1035-
CHECK(j.type() == json::value_t::array);
1040+
CHECK(j.type() == json::value_t::number_float);
10361041
}
10371042
}
10381043
}
@@ -1110,7 +1115,7 @@ TEST_CASE("constructors")
11101115
std::string source(1024, '!');
11111116
const auto* source_addr = source.data();
11121117
json j = {std::move(source)};
1113-
const auto* target_addr = j[0].get_ref<std::string const&>().data();
1118+
const auto* target_addr = j.get_ref<std::string const&>().data();
11141119
const bool success = (target_addr == source_addr);
11151120
CHECK(success);
11161121
}
@@ -1145,7 +1150,7 @@ TEST_CASE("constructors")
11451150
json::array_t source = {1, 2, 3};
11461151
const auto* source_addr = source.data();
11471152
json j {std::move(source)};
1148-
const auto* target_addr = j[0].get_ref<json::array_t const&>().data();
1153+
const auto* target_addr = j.get_ref<json::array_t const&>().data();
11491154
const bool success = (target_addr == source_addr);
11501155
CHECK(success);
11511156
}
@@ -1165,7 +1170,7 @@ TEST_CASE("constructors")
11651170
json::array_t source = {1, 2, 3};
11661171
const auto* source_addr = source.data();
11671172
json j = {std::move(source)};
1168-
const auto* target_addr = j[0].get_ref<json::array_t const&>().data();
1173+
const auto* target_addr = j.get_ref<json::array_t const&>().data();
11691174
const bool success = (target_addr == source_addr);
11701175
CHECK(success);
11711176
}
@@ -1188,7 +1193,7 @@ TEST_CASE("constructors")
11881193
json::object_t source = {{"hello", "world"}};
11891194
const json* source_addr = &source.at("hello");
11901195
json j {std::move(source)};
1191-
CHECK(&(j[0].get_ref<json::object_t const&>().at("hello")) == source_addr);
1196+
CHECK(&(j.get_ref<json::object_t const&>().at("hello")) == source_addr);
11921197
}
11931198

11941199
SECTION("constructor with implicit types (object)")
@@ -1204,7 +1209,7 @@ TEST_CASE("constructors")
12041209
json::object_t source = {{"hello", "world"}};
12051210
const json* source_addr = &source.at("hello");
12061211
json j = {std::move(source)};
1207-
CHECK(&(j[0].get_ref<json::object_t const&>().at("hello")) == source_addr);
1212+
CHECK(&(j.get_ref<json::object_t const&>().at("hello")) == source_addr);
12081213
}
12091214

12101215
SECTION("assignment with implicit types (object)")

tests/src/unit-modifiers.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,7 @@ TEST_CASE("modifiers")
310310
auto& x = j.emplace_back(3, "foo");
311311
CHECK(x == json({"foo", "foo", "foo"}));
312312
CHECK(j.type() == json::value_t::array);
313-
CHECK(j == json({{"foo", "foo", "foo"}}));
313+
CHECK(j == json::array({json({"foo", "foo", "foo"})}));
314314
}
315315
}
316316

tests/src/unit-regression2.cpp

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -844,7 +844,7 @@ TEST_CASE("regression tests 2")
844844
SECTION("std::tuple")
845845
{
846846
{
847-
const json j = {9};
847+
const json j = json::array({9});
848848
auto t = j.get<std::tuple<NonDefaultConstructible>>();
849849
CHECK(std::get<0>(t).x == 9);
850850
}
@@ -1119,6 +1119,57 @@ TEST_CASE("regression tests 2")
11191119
CHECK((decoded == json_4804::array()));
11201120
}
11211121
#endif
1122+
1123+
SECTION("copy constructor changes semantics with brace initialization")
1124+
{
1125+
// object
1126+
{
1127+
json const j_obj = {{"key", "value"}, {"num", 42}};
1128+
json const j{j_obj};
1129+
CHECK(j.is_object());
1130+
CHECK(j == j_obj);
1131+
}
1132+
1133+
// array
1134+
{
1135+
json const j_arr = {1, 2, 3};
1136+
json const j{j_arr};
1137+
CHECK(j.is_array());
1138+
CHECK(j == j_arr);
1139+
}
1140+
1141+
// primitives
1142+
{
1143+
json const j_bool{true};
1144+
CHECK(j_bool.is_boolean());
1145+
1146+
json const j_num{42};
1147+
CHECK(j_num.is_number_integer());
1148+
1149+
json const j_float{3.14};
1150+
CHECK(j_float.is_number_float());
1151+
1152+
json const j_str{"hello"};
1153+
CHECK(j_str.is_string());
1154+
}
1155+
1156+
// passing by-value should still work correctly
1157+
{
1158+
auto receive_by_value = [](json j) { return j; };
1159+
json const j_obj = {{"key", "value"}};
1160+
json const result = receive_by_value(j_obj);
1161+
CHECK(result.is_object());
1162+
CHECK(result == j_obj);
1163+
}
1164+
1165+
// explicitly creating a single-element array still works via json::array()
1166+
{
1167+
json const j_arr = json::array({42});
1168+
CHECK(j_arr.is_array());
1169+
CHECK(j_arr.size() == 1);
1170+
CHECK(j_arr[0] == 42);
1171+
}
1172+
}
11221173
}
11231174

11241175
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)

0 commit comments

Comments
 (0)