Skip to content

Share one nesting depth limit between all bounded descents - #5637

Merged
nlohmann merged 1 commit into
developfrom
unify-depth-limits
Sep 30, 2026
Merged

nlohmann merged 1 commit into
developfrom
unify-depth-limits

Conversation

@nlohmann

Copy link
Copy Markdown
Owner

Summary

@gregmarr pointed out in #5393 (comment) that there are still two depth limits for the bounded descents (recursing up to a fixed depth, then continuing on an explicit stack):

Both were 128, but nothing kept them in sync. This PR deletes nesting_depth_limit(), so the thread-local counter is checked against detail::recursion_depth_limit() too.

The two mechanisms stay as they are. The copy constructor and the comparison operators have fixed signatures and can't take a depth argument, so they still count depth in a thread-local byte. Only the limit is shared. Because that count is a byte and can go one level past the limit, a static_assert requires recursion_depth_limit() < 255. Raising the limit to 255 locally trips it.

The documentation of recursion_depth_limit() now says that copying and comparing use it too, and gives the reason for the byte-size restriction.

Testing

  • unit-comparison, unit-constructor1, unit-diagnostics, unit-merge_patch, unit-hash: pass (clang, C++17, -Wall -Wextra -Werror)
  • -DJSON_NO_THREAD_LOCAL -DJSON_DIAGNOSTICS=1: compiles
  • make amalgamate run

Public API

No breaking changes. nesting_depth_limit() was a private member of basic_json. The limit stays at 128, so behavior is unchanged.


Written by Claude Code.

🤖 Generated with Claude Code

Copying and comparing stopped their descent at
basic_json::nesting_depth_limit(), while serializing, hashing and
merging used detail::recursion_depth_limit(). Both were 128, but
nothing kept them equal. The thread-local count now tests against
detail::recursion_depth_limit() as well, and a static_assert keeps
the limit small enough for the byte that holds the count.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
@nlohmann nlohmann added the review needed It would be great if someone could review the proposed changes. label Sep 29, 2026
@nlohmann nlohmann added 🚀 ready to merge Ready to merge - just waiting for CI to complete. and removed review needed It would be great if someone could review the proposed changes. labels Sep 29, 2026
@nlohmann nlohmann added this to the Release 3.13.0 milestone Sep 29, 2026
@github-actions github-actions Bot added the M label Sep 30, 2026
@nlohmann
nlohmann merged commit 0050d0f into develop Sep 30, 2026
162 of 163 checks passed
@nlohmann
nlohmann deleted the unify-depth-limits branch September 30, 2026 18:06
nlohmann added a commit that referenced this pull request Sep 30, 2026
The comment still named nesting_depth_limit, which #5637 removed on
develop in favor of detail::recursion_depth_limit().

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
nlohmann added a commit that referenced this pull request Sep 30, 2026
)

* Fix stack overflow converting deep values between specializations

Constructing a basic_json from another specialization (json to
ordered_json or back, also via get<ordered_json>()) converted every
container with its range constructor, which calls the converting
constructor for each element. The call stack therefore grew with every
nesting level, and a value nested some 30,000 levels deep overflowed it.

The conversion now bounds its descent the way the copy constructor does
since #5387: the first 128 levels are converted exactly as before, and
below that convert_iteratively() finishes the value with an explicit
stack. It builds each container bottom-up from its converted elements
with the container's range constructor, so member order and keys that
become equal are handled as before, and it gives a value its type only
once its container exists, so an exception leaves nothing behind that
cannot be destroyed. Parents (JSON_DIAGNOSTICS) and positions
(JSON_DIAGNOSTIC_POSITIONS) are set for every value.

Converting a null value no longer resets its positions: the constructor
assigned null to a value that already was null, which swapped in the
positions of the temporary.

Fixes #5650.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Explain why converting null keeps positions and why next is a reference

Review feedback on #5723 (gregmarr): clarify in comments that the
converting constructor has already copied the positions of val, which
the null case keeps like every other case, and that next must be a
reference into pending so that ++next advances the stored iterator.

Comments only; no code change.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Refer to recursion_depth_limit() in the convert_structured() docs

The comment still named nesting_depth_limit, which #5637 removed on
develop in favor of detail::recursion_depth_limit().

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Advance the pending iterator through pending.back() and shorten the null comment

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

M 🚀 ready to merge Ready to merge - just waiting for CI to complete.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants