Repository navigation
Do not search a container for the value the callback rejected - #5457
Merged
nlohmann merged 1 commit intoSep 11, 2026
Merged
Conversation
nlohmann
force-pushed
the
claude/callback-parse-quadratic
branch
from
September 3, 2026 04:44
ec93161 to
d05b377
Compare
nlohmann
force-pushed
the
claude/callback-parse-quadratic
branch
2 times, most recently
from
September 5, 2026 08:25
65d5e66 to
39263b4
Compare
4 tasks done
nlohmann
force-pushed
the
claude/callback-parse-quadratic
branch
from
September 9, 2026 11:17
39263b4 to
41300fe
Compare
nlohmann
force-pushed
the
claude/callback-parse-quadratic
branch
from
September 10, 2026 15:20
41300fe to
d34ea13
Compare
nlohmann
force-pushed
the
claude/callback-parse-quadratic
branch
2 times, most recently
from
September 10, 2026 15:28
3b7ad7b to
e979670
Compare
nlohmann
force-pushed
the
claude/callback-parse-quadratic
branch
from
September 10, 2026 15:28
3b7ad7b to
e979670
Compare
nlohmann
force-pushed
the
claude/callback-parse-quadratic
branch
from
September 10, 2026 15:49
e979670 to
7376a9c
Compare
gregmarr
approved these changes
Sep 10, 2026
nlohmann
force-pushed
the
claude/callback-parse-quadratic
branch
from
September 10, 2026 16:01
7376a9c to
797f61c
Compare
nlohmann
force-pushed
the
claude/callback-parse-quadratic
branch
2 times, most recently
from
September 10, 2026 17:01
daf765b to
0ed313c
Compare
When a parser callback rejects a value, the placeholder stored for it has to
be removed from its parent again. remove_discarded_value() found it by
scanning the parent from the beginning, so filtering a container cost one
scan per rejected member - quadratic in the number of members of a single
container.
A rejected value can only ever be the one most recently added to its parent:
the last element of an array, or the placeholder key() stored under the
current key in an object. Record that key alongside the existing
key_keep_stack, and for a container record it again alongside ref_stack so
end_object()/end_array() can find it in the parent. Removal is then O(1) for
an array and O(log n) for an object, and finding nothing there means nothing
was stored, so there is nothing to remove.
The key for a container is read before handle_value() may consume it, so it
is also correct when the callback rejects the container at its start event
and it never reaches its parent at all.
Discarding half the members of one object, before -> after:
members value rejected container rejected at start
16 000 392 ms -> 3.7 ms 803 ms -> 8.2 ms
64 000 6238 ms -> 14.6 ms 12651 ms -> 32.2 ms
128 000 25339 ms -> 30.6 ms
Results are unchanged: 48 000 randomized documents parsed under 12 different
filtering callbacks - covering duplicate keys, empty keys, rejected keys and
containers rejected at both their start and end events - produce byte
identical output before and after.
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
nlohmann
force-pushed
the
claude/callback-parse-quadratic
branch
from
September 11, 2026 06:19
0ed313c to
14d9958
Compare
1 of 2 tasks
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.
json::parse(input, callback)is quadratic in the number of members of a single container when the callback rejects many of them.Stack
When the callback rejects a value, the placeholder stored for it has to be removed from its parent again.
remove_discarded_value()found it by scanning the parent from the beginning:so filtering a container costs one full scan per rejected member. Discarding half the members of one object:
The flat
ms / n²column is the quadratic. A 128k-member object takes 25 seconds to filter; a million-member one extrapolates to roughly 25 minutes.The fix
A rejected value can only ever be the one most recently added to its parent: the last element of an array, or the placeholder
key()stored under the current key in an object. Record that key alongside the existingkey_keep_stack, and for a container record it again alongsideref_stacksoend_object()/end_array()can find it in the parent. Removal becomes O(1) for an array and O(log n) for an object.Finding no discarded value there means none was stored in the first place — the callback rejected it before it reached its parent — so there is nothing to remove and no scan is needed.
The key for a container is read before
handle_value()may consume it, so it is also correct when the callback rejects the container at itsobject_start/array_startevent and it never reaches its parent at all. That path was quadratic too:Both are now linear in
n(time per member is flat).Correctness
nst_json_testsuite2) yields identical values and identical error messages on both the contiguous and the streaming adapter.Public API impact
No breaking changes.
json::parse(input, callback)keeps its signature and its behavior; every document parses to exactly the same value as before, and the same inputs still throw the same exceptions with the same messages.The observable complexity of that documented API improves, from quadratic to linear in the number of members of a container. That is a behavior change only in the sense that inputs which previously took minutes now complete.
All changes are confined to
detail::json_sax_dom_callback_parser, which is internal. Its privateremove_discarded_value()gains a parameter and three private members are added; the class is not part of the documented API and cannot be usefully instantiated by callers.This pull request was prepared by Claude Code.