diff --git a/include/nlohmann/json.hpp b/include/nlohmann/json.hpp index 235e6b737..4e057068c 100644 --- a/include/nlohmann/json.hpp +++ b/include/nlohmann/json.hpp @@ -3516,7 +3516,10 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec if (merge_objects && it.value().is_object()) { auto it2 = m_data.m_value.object->find(it.key()); - if (it2 != m_data.m_value.object->end()) + // Only recurse when the existing value is itself an object. + // Otherwise overwrite, matching the documented "all other values + // are overwritten as usual" behavior (see #5402). + if (it2 != m_data.m_value.object->end() && it2->second.is_object()) { it2->second.update(it.value(), true); #if JSON_DIAGNOSTICS diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 904d8659c..4c346b412 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -24870,7 +24870,10 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec if (merge_objects && it.value().is_object()) { auto it2 = m_data.m_value.object->find(it.key()); - if (it2 != m_data.m_value.object->end()) + // Only recurse when the existing value is itself an object. + // Otherwise overwrite, matching the documented "all other values + // are overwritten as usual" behavior (see #5402). + if (it2 != m_data.m_value.object->end() && it2->second.is_object()) { it2->second.update(it.value(), true); #if JSON_DIAGNOSTICS diff --git a/tests/src/unit-modifiers.cpp b/tests/src/unit-modifiers.cpp index 18185ec00..de14b3f70 100644 --- a/tests/src/unit-modifiers.cpp +++ b/tests/src/unit-modifiers.cpp @@ -801,6 +801,30 @@ TEST_CASE("modifiers") j1.update(j2, true); CHECK(j1 == json({{"string", "t"}, {"numbers", 1}})); } + + SECTION("overwrite primitive with object") + { + json j1 = {{"k", 1}}; + json const j2 = {{"k", {{"x", 2}}}}; + j1.update(j2, true); + CHECK(j1 == json({{"k", {{"x", 2}}}})); + } + + SECTION("overwrite array with object") + { + json j1 = {{"k", {1, 2}}}; + json const j2 = {{"k", {{"x", 2}}}}; + j1.update(j2, true); + CHECK(j1 == json({{"k", {{"x", 2}}}})); + } + + SECTION("overwrite nested primitive with object") + { + json j1 = {{"k", {{"inner", 1}}}}; + json const j2 = {{"k", {{"inner", {{"x", 2}}}}}}; + j1.update(j2, true); + CHECK(j1 == json({{"k", {{"inner", {{"x", 2}}}}}})); + } } } } diff --git a/tests/src/unit-regression2.cpp b/tests/src/unit-regression2.cpp index 29b52d701..2e7450e2e 100644 --- a/tests/src/unit-regression2.cpp +++ b/tests/src/unit-regression2.cpp @@ -1555,4 +1555,15 @@ TEST_CASE("issue #5338 - truncated CBOR tagged binary subtype is rejected") } } +TEST_CASE("issue #5402 - update(merge_objects=true) overwrites a primitive with an object") +{ + json t = {{"k", 1}}; + t.update(json{{"k", {{"x", 2}}}}, true); + CHECK(t == json({{"k", {{"x", 2}}}})); + + json mixed = {{"keep", {{"a", 1}}}, {"replace", 1}}; + mixed.update(json{{"keep", {{"b", 2}}}, {"replace", {{"x", 2}}}}, true); + CHECK(mixed == json({{"keep", {{"a", 1}, {"b", 2}}}, {"replace", {{"x", 2}}}})); +} + DOCTEST_CLANG_SUPPRESS_WARNING_POP