Compare commits

...
Author SHA1 Message Date
Niels Lohmann 9802542843 Take diff()'s fast path unless the object type reorders members
For every object type except an insertion-ordered one like ordered_map,
diff() no longer produced a member-by-member patch when target had a key
that sorts before a key the two objects share: it fell through to the
slow path, which removes every member of source and re-adds every member
of target, instead of just adding the new key.

#5465 added an order check to require the fast path to also reproduce
target's member order, needed because ordered_map's patch()-driven "add"
appends a new member at the end. The check compared the common keys'
order between source and target and also required that every added key
come after every common key in target's order ("new_keys_form_suffix").
The comment above it argued this check is always true for std::map, and
that reasoning is correct for the order of the common keys themselves,
but not for new_keys_form_suffix: a std::map iterates in sorted key
order, so a new key that sorts before an existing common key is
enumerated between common keys, making new_keys_form_suffix false even
though std::map's own key order does not need reordering at all - it
places every member itself, regardless of insertion history, so a
member-by-member diff already reproduces target's iteration order.

Only require the order check for an object type that keeps insertion
order, using the same detail::is_ordered_map trait the library already
uses to recognize such an object type in set_parent(). Every other
object type - std::map in key order, a hash map in an order its
operator== ignores - always takes the fast path.

Added a regression test to unit-json_patch.cpp: the issue's example now
yields a single "add" op for json, while ordered_json still takes the
slow path to reproduce target's member order.

Fixes #5639.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-29 23:35:53 +02:00
3 changed files with 68 additions and 2 deletions
+8 -1
View File
@@ -6187,7 +6187,14 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
}
}
if (common_keys_source_order == common_keys_target_order && new_keys_form_suffix)
// Only an object type that keeps its members in insertion
// order, such as nlohmann::ordered_map, can need reordering:
// patch() appends a new member at the end of such an object.
// Any other object type places its members itself - std::map
// in key order, a hash map in an order its operator== ignores -
// so a member-by-member diff always reproduces target there.
if (!detail::is_ordered_map<object_t>::value
|| (common_keys_source_order == common_keys_target_order && new_keys_form_suffix))
{
// fast path: order of common keys already matches (or the
// object_t's iteration order does not depend on
+8 -1
View File
@@ -32268,7 +32268,14 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
}
}
if (common_keys_source_order == common_keys_target_order && new_keys_form_suffix)
// Only an object type that keeps its members in insertion
// order, such as nlohmann::ordered_map, can need reordering:
// patch() appends a new member at the end of such an object.
// Any other object type places its members itself - std::map
// in key order, a hash map in an order its operator== ignores -
// so a member-by-member diff always reproduces target there.
if (!detail::is_ordered_map<object_t>::value
|| (common_keys_source_order == common_keys_target_order && new_keys_form_suffix))
{
// fast path: order of common keys already matches (or the
// object_t's iteration order does not depend on
+52
View File
@@ -1752,6 +1752,58 @@ TEST_CASE("JSON patch - diff emits array removals in descending index order")
}
}
TEST_CASE("JSON patch - diff() takes the fast path for non-reorderable object types (regression #5639)")
{
// #5465 added an order check to diff()'s object handling so a
// member-by-member diff is only used when it would also reproduce
// target's member *order* -- needed for ordered_json, whose object_t
// keeps insertion order and whose patch() "add" op appends a new
// member at the end. For json's default object_t (std::map, which
// orders members by key regardless of insertion history), that check
// could still fail: a new key that sorts before an existing common key
// makes target's iteration interleave the new key between common keys,
// even though nothing else about the object changed. That sent the
// whole object through the slow (remove-every-member,
// re-add-every-member) path instead of the minimal one.
SECTION("json: added key sorts before an existing common key")
{
const json source = {{"a", 1}, {"c", {{"x", 1}, {"y", 2}}}};
const json target = {{"a", 1}, {"b", 0}, {"c", {{"x", 1}, {"y", 2}}}};
const json patch = json::diff(source, target);
// only the new key is added; "a" and "c" are left alone instead of
// being removed and re-added
const json expected = R"([{"op": "add", "path": "/b", "value": 0}])"_json;
CHECK(patch == expected);
CHECK(source.patch(patch) == target);
}
SECTION("ordered_json: reordering behavior from #5465 is unchanged")
{
using nlohmann::ordered_json;
// same key/value shape as the json case above, but for ordered_json
// the *target*'s member order must be reproduced, so the slow path
// is still required here.
ordered_json source;
source["a"] = 1;
source["c"] = ordered_json{{"x", 1}, {"y", 2}};
ordered_json target;
target["a"] = 1;
target["b"] = 0;
target["c"] = ordered_json{{"x", 1}, {"y", 2}};
const ordered_json patch = ordered_json::diff(source, target);
// unlike the json case: every member is still removed and re-added
// so the result ends up in target's order (2 removes + 3 adds)
CHECK(patch.size() == 5);
CHECK(source.patch(patch) == target);
}
}
TEST_CASE("JSON patch - every operation on ordered_json")
{
using nlohmann::ordered_json;