Repository navigation
Keep JSON_DIAGNOSTICS parent pointers of ordered_json members after erase() and update() - #5552
Merged
Merged
Conversation
nlohmann
force-pushed
the
claude/silly-rhodes-f0f089
branch
from
September 24, 2026 15:14
af31bf0 to
f3ee159
Compare
…rase() and update() ordered_json stores its members in a vector, and two operations moved members without restoring their parent pointers afterwards: - ordered_map::erase() re-constructs every member after the erased one in place. The basic_json move constructor leaves m_parent at nullptr, and none of the object branches of basic_json::erase() (by key, iterator, or iterator range) called set_parents(). This also affected merge_patch() with a null member and patch() with a remove operation. - update() only set the parent pointer of the inserted member. Adding a key can reallocate the vector, which copies all other members and leaves their m_parent at nullptr. The set_parents() call added for #4813 only repaired this for the nested object of a merge, not for the target. The next assert_invariant() on such an object (for instance, when copying it) aborted, and diagnostic messages lost the path prefix above the moved member. std::map-based json was not affected, because its nodes do not move. Erasing from an ordered_map object now calls set_parents(), and update() uses set_parent(), which already refreshes all members for vector-based objects. This makes the #4813 workaround redundant. Signed-off-by: Niels Lohmann <mail@nlohmann.me>
…ter test The merge_patch() case parses its input, so with JSON_DIAGNOSTIC_POSITIONS the exception message also carries the byte range of the parsed value. Signed-off-by: Niels Lohmann <mail@nlohmann.me>
…t-pointer test Signed-off-by: Niels Lohmann <mail@nlohmann.me>
The iterative path of update() only set the parent pointer of the member it inserted, like the recursive one did before. It now uses set_parent() too, so ordered_json members that move when a nested object grows keep their parents, and the set_parents() calls that patched this up after each nested merge are gone. Signed-off-by: Niels Lohmann <mail@nlohmann.me>
nlohmann
force-pushed
the
claude/silly-rhodes-f0f089
branch
from
September 24, 2026 15:19
f3ee159 to
3993137
Compare
gregmarr
approved these changes
Sep 24, 2026
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
nlohmann
added a commit
that referenced
this pull request
Sep 25, 2026
Resolve the conflict in json.hpp by keeping both the comparison helpers and set_parents_after_object_erase() from #5552, and regenerate the amalgamation. Signed-off-by: Niels Lohmann <mail@nlohmann.me>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
With
JSON_DIAGNOSTICSenabled,erase()andupdate()can leave members of anordered_jsonobject with a null parent pointer. The nextassert_invariant()on that object then aborts, for example when the object is copied. InNDEBUGbuilds nothing aborts, but diagnostic messages silently lose part of the path, e.g.(/x)instead of(/b/x).nlohmann::jsonisn't affected, becausestd::mapdoesn't move its entries.Minimal reproducer (aborts on
develop):Root cause
ordered_jsonstores its members in a vector. Two operations move members without restoring their parent pointers:ordered_map::eraserebuilds every later member in place (destroy + placement-new). Thebasic_jsonmove constructor doesn't carrym_parentover, and none of the object branches ofbasic_json::erase(by key, iterator or range) repaired this.merge_patch()with anullmember andpatch()with aremoveoperation go through the same code.update()adding a key that makes the vector grow. Reallocation copy-constructs the other members, becausestd::pair<const std::string, basic_json>isn't nothrow-movable. Those copies get a null parent, andupdate()only set the parent of the member it inserted. Theit2->second.set_parents()added for json::update() with merge_objects==true may trigger JSON_ASSERT for some objects #4813 repaired this only for the nested object during a merge, not for the objectupdate()is called on. The json::update() with merge_objects==true may trigger JSON_ASSERT for some objects #4813 test still passed because it only usesCHECK_NOTHROWand never copies the result.In the reproducer,
/a/cbecomes[d, c], and erasingdshiftscinto slot 0 with a null parent. The assertion fires when insertingemakes/a's vector reallocate: that reallocation copies/a/c, and the copy constructor checks the source.Changes
set_parents_after_object_erase(), runsset_parents()after an erase if the object type is vector-backed (detail::is_ordered_map). It does nothing unlessJSON_DIAGNOSTICSis on. All four object erase paths call it: botherase_internaloverloads,erase(iterator)anderase(iterator, iterator).update()now uses the existingset_parent(), which already refreshes all members for vector-backed objects. This makes the json::update() with merge_objects==true may trigger JSON_ASSERT for some objects #4813 workaround redundant, so it's removed.tests/src/unit-diagnostics.cppcoverserase(key, iterator, range),patchremove,update(j),update(j, true)and the reproducer above, all onordered_json. Each case checks two things: a diagnostic path read through const access (sooperator[]can't repair the pointers; this catches the bug underNDEBUG) and a deep copy (which runsassert_invariant()on every node).Testing
NDEBUG, six of its checks fail on the truncated path.operator[], alleraseforms,update,merge_patch,patch,emplaceandinsertonordered_json, with a deep copy after every step) passes with the fix. The old header aborts almost immediately.-DJSON_DIAGNOSTICS=1, give the same results as the old header. The only difference is the new test.-Wall -Wextra -Wpedantic -Werror. GCC wasn't tested locally.Interaction with #5547
#5547's
merge_patchrewrite erases throughbasic_json::erase, so it picks up this fix automatically. Itsupdate_members()andupdate_members_iteratively()still use the oldoperator[](key).m_parent = ...pattern, and the iterative version never repairs the top-level object. When #5547 is rebased onto this, both functions should switch toset_parent(...)and drop theirset_parents()workarounds.Breaking changes
None. The public API is unchanged: the new helper is private, and the behavior only changes when
JSON_DIAGNOSTICSis enabled.make amalgamate.This pull request was written by Claude Code.
🤖 Generated with Claude Code