From 7fd68957886258edeec2bd14a367f33ab56002ac Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Wed, 30 Sep 2026 20:07:48 +0200 Subject: [PATCH] Hide a discarded container's content from the parser callback (#5706) When a parser callback rejects an object's or array's start event, json_sax_dom_callback_parser kept calling it for everything inside that container anyway: nested keys, values, and the start/end events of containers below it. This contradicts parser_callback_t's own documentation, which promises that discarding a container at its start event also hides its content from the callback. The same code path also kept a full copy of every key inside such a discarded container in key_stack until the whole parse finished, because the early return for values that are not stored skipped the matching pop. Filtering out a large subtree is the main reason to use a callback, so this made peak memory during the parse scale with the size of the very subtree the callback was trying to skip. Fix start_object(), start_array(), and key() so that a container whose own start event was discarded, or that is nested inside one, is never handed to the callback, and no longer pushes onto the key stacks. A container whose start event was accepted but whose key was rejected still gets its content reported, as documented ("the callback is still called for the associated value, but its return value has no further effect"); only its own bookkeeping is skipped since it will not be stored. Fixes #5643. Signed-off-by: Niels Lohmann --- .../docs/api/basic_json/parser_callback_t.md | 3 + include/nlohmann/detail/input/json_sax.hpp | 18 +++- single_include/nlohmann/json.hpp | 18 +++- tests/src/unit-class_parser.cpp | 96 +++++++++++++++++++ 4 files changed, 129 insertions(+), 6 deletions(-) diff --git a/docs/mkdocs/docs/api/basic_json/parser_callback_t.md b/docs/mkdocs/docs/api/basic_json/parser_callback_t.md index da23e9bc2..c1a35afb9 100644 --- a/docs/mkdocs/docs/api/basic_json/parser_callback_t.md +++ b/docs/mkdocs/docs/api/basic_json/parser_callback_t.md @@ -100,3 +100,6 @@ the latter case, it is skipped completely, or replaced by `null` if it is the to - Added in version 1.0.0. - Fixed in version 3.13.0 to also remove discarded values from a parent object; before, discarding an array or a value stored under an object key left a discarded member behind, which made the parse result serialize to invalid JSON. +- Fixed in version 3.13.0 so that discarding an array or object at its start event also hides its content from the + callback, as documented above; before, the callback was still called for the content, and the key of every member of + a discarded object was kept in memory until the parse ended. diff --git a/include/nlohmann/detail/input/json_sax.hpp b/include/nlohmann/detail/input/json_sax.hpp index 962913610..8b8f544eb 100644 --- a/include/nlohmann/detail/input/json_sax.hpp +++ b/include/nlohmann/detail/input/json_sax.hpp @@ -582,8 +582,8 @@ class json_sax_dom_callback_parser bool start_object(std::size_t len) { - // check callback for object start - const bool keep = callback(static_cast(ref_stack.size()), parse_event_t::object_start, discarded); + // check callback for object start; not called inside a discarded container + const bool keep = keep_stack.back() && callback(static_cast(ref_stack.size()), parse_event_t::object_start, discarded); keep_stack.push_back(keep); // the key this object will be stored under, read before handle_value() @@ -619,6 +619,18 @@ class json_sax_dom_callback_parser bool key(string_t& val) { + if (!keep_stack.back() || !ref_stack.back()) + { + // the object is not stored: the value of this key is dropped in + // handle_value() without touching the key stacks + if (keep_stack.back()) + { + BasicJsonType k = BasicJsonType(val); + static_cast(callback(static_cast(ref_stack.size()), parse_event_t::key, k)); + } + return true; + } + BasicJsonType k = BasicJsonType(val); // check callback for the key @@ -704,7 +716,7 @@ class json_sax_dom_callback_parser bool start_array(std::size_t len) { - const bool keep = callback(static_cast(ref_stack.size()), parse_event_t::array_start, discarded); + const bool keep = keep_stack.back() && callback(static_cast(ref_stack.size()), parse_event_t::array_start, discarded); keep_stack.push_back(keep); // see start_object() diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 8d48e643a..5a4595956 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -12757,8 +12757,8 @@ class json_sax_dom_callback_parser bool start_object(std::size_t len) { - // check callback for object start - const bool keep = callback(static_cast(ref_stack.size()), parse_event_t::object_start, discarded); + // check callback for object start; not called inside a discarded container + const bool keep = keep_stack.back() && callback(static_cast(ref_stack.size()), parse_event_t::object_start, discarded); keep_stack.push_back(keep); // the key this object will be stored under, read before handle_value() @@ -12794,6 +12794,18 @@ class json_sax_dom_callback_parser bool key(string_t& val) { + if (!keep_stack.back() || !ref_stack.back()) + { + // the object is not stored: the value of this key is dropped in + // handle_value() without touching the key stacks + if (keep_stack.back()) + { + BasicJsonType k = BasicJsonType(val); + static_cast(callback(static_cast(ref_stack.size()), parse_event_t::key, k)); + } + return true; + } + BasicJsonType k = BasicJsonType(val); // check callback for the key @@ -12879,7 +12891,7 @@ class json_sax_dom_callback_parser bool start_array(std::size_t len) { - const bool keep = callback(static_cast(ref_stack.size()), parse_event_t::array_start, discarded); + const bool keep = keep_stack.back() && callback(static_cast(ref_stack.size()), parse_event_t::array_start, discarded); keep_stack.push_back(keep); // see start_object() diff --git a/tests/src/unit-class_parser.cpp b/tests/src/unit-class_parser.cpp index 52b49c5d9..2d4ff6a38 100644 --- a/tests/src/unit-class_parser.cpp +++ b/tests/src/unit-class_parser.cpp @@ -1990,6 +1990,102 @@ TEST_CASE("parser class") } } + SECTION("no callback for the content of a discarded container (#5643)") + { + // discarding a container at its start event must also hide + // everything inside it from the callback: none of the nested + // keys, values, or nested containers' own start/end events may + // be reported + std::vector log; + bool first = true; + const json j = json::parse(R"({"skip": {"k1": 1, "k2": [2, {"k3": 3}]}, "keep": 1})", + [&](int depth, json::parse_event_t event, json & parsed) + { + static const char* const names[] = {"object_start", "object_end", "array_start", "array_end", "key", "value"}; + log.push_back(std::to_string(depth) + " " + names[static_cast(event)] + " " + parsed.dump()); + + if (depth == 1 && event == json::parse_event_t::object_start && first) + { + // discard "skip" right at its object_start event + first = false; + return false; + } + return true; + }); + + CHECK(log == std::vector + { + "0 object_start ", + "1 key \"skip\"", + "1 object_start ", + "1 key \"keep\"", + "1 value 1", + "0 object_end {\"keep\":1}" + }); + CHECK(j == json({{"keep", 1}})); + } + + SECTION("callback still called inside a container whose key was rejected (#5643)") + { + // rejecting a key does not discard its value's container at the + // container's own start event, so the callback is still called + // for that container's content; only storing the container + // under the rejected key is skipped + // (documented for parser_callback_t: "the callback is still + // called for the associated value, but its return value has no + // further effect") + const auto record = [](std::vector& log, int depth, json::parse_event_t event, const json & parsed) + { + static const char* const names[] = {"object_start", "object_end", "array_start", "array_end", "key", "value"}; + log.push_back(std::to_string(depth) + " " + names[static_cast(event)] + " " + parsed.dump()); + }; + + std::vector log_object; + const json j_object = json::parse(R"({"skip": {"k1": 1}, "keep": 2})", + [&](int depth, json::parse_event_t event, json & parsed) + { + record(log_object, depth, event, parsed); + return !(event == json::parse_event_t::key && parsed == json("skip")); + }); + + CHECK(log_object == std::vector + { + "0 object_start ", + "1 key \"skip\"", + "1 object_start ", + "2 key \"k1\"", + "2 value 1", + "1 key \"keep\"", + "1 value 2", + "0 object_end {\"keep\":2}" + }); + CHECK(j_object == json({{"keep", 2}})); + + // same for a rejected key whose value is an array rather than an object + std::vector log_array; + const json j_array = json::parse(R"({"skip": [1, {"k1": 2}], "keep": 2})", + [&](int depth, json::parse_event_t event, json & parsed) + { + record(log_array, depth, event, parsed); + return !(event == json::parse_event_t::key && parsed == json("skip")); + }); + + CHECK(log_array == std::vector + { + "0 object_start ", + "1 key \"skip\"", + "1 array_start ", + "2 value 1", + "2 object_start ", + "3 key \"k1\"", + "3 value 2", + "1 key \"keep\"", + "1 value 2", + "0 object_end {\"keep\":2}" + }); + CHECK(j_array == json({{"keep", 2}})); + } + SECTION("special cases") { // the following test cases cover the situation in which an empty