JSON for Modern C++(nlohmann/json)SAX 接口详解:json_sax::end_object 对象结束事件与解析中止机制
【免费下载链接】jsonJSON for Modern C++项目地址: https://gitcode.com/GitHub_Trending/js/json
本篇基于 JSON for Modern C++(nlohmann/json) 官方 API 文档中的 json_sax::end_object 条目展开,讲解json_sax纯虚回调家族中负责"对象结束"的事件如何被文本解析器与二进制格式解析器触发、其返回值如何控制整个sax_parse流程是否继续,并借助仓库头文件、单元测试与官方示例,让你能够独立实现一个正确实现end_object()的自定义 SAX 消费者,并用它完成流式解析、提前中止与事件计数等任务。
一、end_object在 SAX 接口家族中的定位
在 nlohmann/json 中,SAX(Simple API for XML 思想在 JSON 上的移植)是一组面向"解析过程事件"的纯虚回调。接口模板定义在头文件 include/nlohmann/detail/input/json_sax.hpp 中:
template<typename BasicJsonType> struct json_sax { // ... virtual bool end_object() = 0; // ... };end_object的函数原型(见 json_sax.hpp)在文档中描述为:
virtual bool end_object() = 0;它的语义一句话即可概括:"一个 JSON object(花括号包裹的键值对容器)已经完整读完"。与之成对出现的是start_object(读到{)、以及逐成员进入的key/value回调;而数组的对应事件是 end_array。完整接口清单可参见 json_sax 索引文档。
json_sax<BasicJsonType>中的所有回调(null、boolean、number_*、string、start_object、key、end_object、start_array、end_array、parse_error等)都是纯虚函数,因此任何自定义消费者都必须逐一实现。为此,basic_json内置了便捷别名(include/nlohmann/json.hpp):
using json_sax_t = json_sax<basic_json>;官方示例(docs/mkdocs/docs/examples/sax_parse.cpp)中正是让自定义类继承json::json_sax_t,其注释也指出"继承并非强制,但可以避免遗漏某个必须实现的函数"。
版本信息:
end_object与整套json_sax接口均从3.2.0版本开始提供(接口对二进制值类型的支持则是在 3.8.0 加入)。
二、何时触发:JSON 解析器中的调用点
2.1 文本 JSON 的调用链
当以默认input_format_t::json文本格式调用 sax_parse 时,实际驱动 SAX 事件的是预测性 LL(1) 文本解析器 include/nlohmann/detail/input/parser.hpp。在对象解析逻辑中,一旦词法分析返回token_type::end_object(即读到}),解析器便回调消费端:
// 对象内部成员序列解析完毕,读到 "}" 时 if (get_token() == token_type::end_object) { if (JSON_HEDLEY_UNLIKELY(!sax->end_object())) { return false; // 消费者要求停止 } // ... 否则继续 }在 parser.hpp 及该文件后面对象收尾的另一处(parser.hpp)可以看到同样的模式:sax->end_object()返回false时解析器立刻return false,不再消费剩余输入。对嵌套对象而言,最内层对象先结束,因此end_object()会从内向外按深度优先顺序被多次回调——输入里有多少层对象闭合,就有多少次end_object()。
2.2 二进制格式同样会产生该事件
nlohmann/json 的 SAX 解析不仅支持 JSON 文本,也支持 CBOR、MessagePack、UBJSON、BJData、BSON 等二进制格式。这些格式的读取器统一在 include/nlohmann/detail/input/binary_reader.hpp 中实现,各格式在解析完一个 object(映射/map 结构)后同样会调用sax->end_object(),例如 binary_reader.hpp(BSON 文档)、以及同文件中 L1309、L1939、L2701、L2842 等多个代表调用点(对应各格式对象收尾逻辑)。也就是说:无论输入是文本 JSON 还是二进制格式,只要 SAX 消费端实现了end_object(),它就能在每次对象结束时收到通知——这正是跨格式统一事件处理的基础。
2.3 静态约束:接口不实现就无法编译
如果自定义 SAX 类型并未继承json_sax,而是仅实现了其中部分函数,编译期检测工具会通过 SFINAE 检查该类型是否具备全部必需签名。include/nlohmann/detail/meta/is_sax.hpp 中定义了:
template<typename T> using end_object_function_t = decltype(std::declval<T&>().end_object()); // ... static_assert(is_detected_exact<bool, end_object_function_t, SAX>::value, "Missing/invalid function: bool end_object()");即要求SAX::end_object()必须存在且精确返回bool(见 is_sax.hpp)。这是"鸭子类型"式接口编译期契约的体现,继承json::json_sax_t可以避免踩坑。
三、返回值语义:sax_parse的提前中止开关
文档对end_object的返回值只有一句话:"Whether parsing should proceed"(是否继续解析)。这句话的内涵需要结合sax_parse的整条数据流理解:
- 所有 SAX 回调(包括
end_object)返回true表示"事件已处理,继续解析下一个 token"; - 返回
false表示消费者主动要求终止,解析器立刻停止并逐层退出; sax_parse的最终返回值等于最后一个被处理的 SAX 事件的返回值(见 sax_parse 文档 的 "Return value" 一节)。
因此end_object()返回false是"在对象闭合处截断解析"的惯用手段。仓库单元测试 tests/src/unit-class_parser.cpp 中的SaxCountdown类就专门验证这一点——它让每个事件把计数器减一:
class SaxCountdown : public nlohmann::json::json_sax_t { public: explicit SaxCountdown(const int count) : events_left(count) {} bool null() override { return events_left-- > 0; } // ... 其他事件类似,均返回 events_left-- > 0 bool end_object() override { return events_left-- > 0; // 计数归零时返回 false,中止解析 } // ... };对应断言(unit-class_parser.cpp):
SECTION("SAX parser") { SECTION("} without value") { SaxCountdown s(1); CHECK(json::sax_parse("{}", &s) == false); // start_object 消耗计数后,end_object 返回 false } SECTION("} with value") { SaxCountdown s(3); CHECK(json::sax_parse("{\"k1\": true}", &s) == false); } // ... }当计数归零、end_object()(或其他事件)返回false时,sax_parse整体返回false。该特性可用于实现"只解析到某个对象边界就停止"的场景,例如从大文件中读取第一个完整配置对象后立即结束,避免继续构建整个 DOM。
四、库内部的三套内置实现对照
理解内置实现有助于设计自定义消费者。同一头文件 json_sax.hpp 中定义了三种标准 SAX 实现,它们的end_object各有分工:
json_sax_dom_parser(DOM 构建器)——json_sax.hpp。普通json::parse()走的就是它:解析过程中用ref_stack维护嵌套层级;end_object()到来时断言栈顶确实是一个 object,可选地记录诊断位置(JSON_DIAGNOSTIC_POSITIONS),调用set_parents()建立父子关系,然后pop_back()弹栈。由此{}结束即代表"该层对象已构造完毕"。json_sax_dom_callback_parser(带回调的 DOM 构建器)——json_sax.hpp。它在end_object()时把已构造好的整个对象交给用户回调(事件类型parse_event_t::object_end);若回调返回false,该对象会被置为discarded并从父容器中移除。这正是json::parse(json, filter_callback)过滤接口的底层机制。json_sax_acceptor(只验证合法性)——json_sax.hpp。json::accept()使用它,所有事件一律返回true,end_object()也不例外,目的是"只检查语法、不建树"。
对比三者可以发现一个通用设计准则:end_object是"对象作用域结束"的边界信号——DOM 构建器在此收束该层节点,回调构建器在此询问用户"要不要保留",校验器则在此确认结构闭合即可。
五、官方示例:事件消费者中实现并验证end_object
文档页以 sax_parse.cpp 作为完整示例,展示如何实现一个记录所有 SAX 事件的自定义消费者并驱动sax_parse。完整源码如下:
#include <iostream> #include <iomanip> #include <sstream> #include <nlohmann/json.hpp> using json = nlohmann::json; // a simple event consumer that collects string representations of the passed // values; note inheriting from json::json_sax_t is not required, but can // help not to forget a required function class sax_event_consumer : public json::json_sax_t { public: std::vector<std::string> events; bool null() override { events.push_back("null()"); return true; } bool boolean(bool val) override { events.push_back("boolean(val=" + std::string(val ? "true" : "false") + ")"); return true; } bool number_integer(number_integer_t val) override { events.push_back("number_integer(val=" + std::to_string(val) + ")"); return true; } bool number_unsigned(number_unsigned_t val) override { events.push_back("number_unsigned(val=" + std::to_string(val) + ")"); return true; } bool number_float(number_float_t val, const string_t& s) override { events.push_back("number_float(val=" + std::to_string(val) + ", s=" + s + ")"); return true; } bool string(string_t& val) override { events.push_back("string(val=" + val + ")"); return true; } bool start_object(std::size_t elements) override { events.push_back("start_object(elements=" + std::to_string(elements) + ")"); return true; } bool end_object() override { events.push_back("end_object()"); return true; } bool start_array(std::size_t elements) override { events.push_back("start_array(elements=" + std::to_string(elements) + ")"); return true; } bool end_array() override { events.push_back("end_array()"); return true; } bool key(string_t& val) override { events.push_back("key(val=" + val + ")"); return true; } bool binary(json::binary_t& val) override { events.push_back("binary(val=[...])"); return true; } bool parse_error(std::size_t position, const std::string& last_token, const json::exception& ex) override { events.push_back("parse_error(position=" + std::to_string(position) + ", last_token=" + last_token + ",\n ex=" + std::string(ex.what()) + ")"); return false; } }; int main() { // a JSON text auto text = R"( { "Image": { "Width": 800, "Height": 600, "Title": "View from 15th Floor", "Thumbnail": { "Url": "http://www.example.com/image/481989943", "Height": 125, "Width": 100 }, "Animated" : false, "IDs": [116, 943, 234, -38793], "DeletionDate": null, "Distance": 12.723374634 } }] )"; // create a SAX event consumer object sax_event_consumer sec; // parse JSON bool result = json::sax_parse(text, &sec); // output the recorded events for (auto& event : sec.events) { std::cout << event << "\n"; } // output the result of sax_parse std::cout << "\nresult: " << std::boolalpha << result << std::endl; }编译运行方式(与仓库单头文件目录配合):
g++ -std=c++11 -I single_include sax_parse.cpp -o sax_parse ./sax_parse官方文档记录的程序输出为(sax_parse.output):
start_object(elements=18446744073709551615) key(val=Image) start_object(elements=18446744073709551615) key(val=Width) number_unsigned(val=800) key(val=Height) number_unsigned(val=600) key(val=Title) string(val=View from 15th Floor) key(val=Thumbnail) start_object(elements=18446744073709551615) key(val=Url) string(val=http://www.example.com/image/481989943) key(val=Height) number_unsigned(val=125) key(val=Width) number_unsigned(val=100) end_object() key(val=Animated) boolean(val=false) key(val=IDs) start_array(elements=18446744073709551615) number_unsigned(val=116) number_unsigned(val=943) number_unsigned(val=234) number_integer(val=-38793) end_array() key(val=DeletionDate) null() key(val=Distance) number_float(val=12.723375, s=12.723374634) end_object() end_object() parse_error(position=460, last_token=12.723374634<U+000A> }<U+000A> }], ex=[json.exception.parse_error.101] parse error at line 17, column 6: syntax error while parsing value - unexpected ']'; expected end of input) result: false对照输出可以提炼出end_object()相关的三个关键观察点:
事件严格嵌套对称:
Thumbnail对象闭合触发一次end_object()(第 18 行输出),Image对象闭合再触发一次,最外层对象闭合触发第三次。三次end_object()与三处start_object()严格配对,体现了 SAX 事件的括号式流结构——如果每次start_object都要压栈,那么每次end_object就应弹栈。JSON 文本不携带元素计数:
start_object(elements=18446744073709551615)中的18446744073709551615正是std::size_t的最大值,即"元素个数未知"的哨兵值(源码中为detail::unknown_size(),见 json_sax.hpp)。如 start_object 文档 所述,只有二进制格式才可能在上报元素数量;JSON 文本解析一律传未知哨兵,所以消费者不能依赖该数值。end_object()之后才轮到根级收尾与错误处理:示例输入在完整对象闭合后还残留一个],sax_parse(默认strict = true)要求输入被完整消费,于是在三次end_object()之后触发parse_error回调,异常信息为[json.exception.parse_error.101] ... unexpected ']'; expected end of input。parse_error回调返回false,sax_parse随之返回false——这与"返回值为最后一个事件的返回值"完全一致。若该输入末尾没有多余的],sax_parse会以true正常结束。
六、实践要点速览
- 必须返回
bool:end_object()的签名必须精确匹配bool end_object(),编译器通过 is_sax.hpp 中的static_assert强制校验;继承json::json_sax_t(json.hpp)可省去逐个核对。 - 用返回值实现"读到对象边界即停":在
end_object()中根据业务条件返回false,sax_parse会立刻停止并返回false;返回true则继续。参考 unit-class_parser.cpp 的SaxCountdown测试类。 - 嵌套对象按闭合顺序触发:对象结束事件从内到外触发;用于计数时需把
end_object与start_object配对理解。 - 适用于全部输入格式:文本 JSON 走 parser.hpp,CBOR/MessagePack/UBJSON/BJData/BSON 走 binary_reader.hpp,两者都会在对象结束时回调
end_object。 - 区分三个内置实现:DOM 构建(
parse)、带过滤回调的 DOM 构建、纯语法校验(accept)对end_object的处理不同,自定义消费者可参考它们各自的实现方式(json_sax.hpp)。 - 版本前提:
end_object自 3.2.0 起可用,与整套json_sax接口同步引入。
如需继续深入 SAX 机制,可依次阅读 json_sax 接口索引、sax_parse 入口文档 以及配套的 start_object 与 end_array 页面,从而完整掌握 SAX 事件流的每个环节。
【免费下载链接】jsonJSON for Modern C++项目地址: https://gitcode.com/GitHub_Trending/js/json
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考