news 2026/9/9 12:32:34

JSON for Modern C++(nlohmann/json)SAX 接口详解:json_sax::end_object 对象结束事件与解析中止机制

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JSON for Modern C++(nlohmann/json)SAX 接口详解:json_sax::end_object 对象结束事件与解析中止机制

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>中的所有回调(nullbooleannumber_*stringstart_objectkeyend_objectstart_arrayend_arrayparse_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各有分工:

  1. json_sax_dom_parser(DOM 构建器)——json_sax.hpp。普通json::parse()走的就是它:解析过程中用ref_stack维护嵌套层级;end_object()到来时断言栈顶确实是一个 object,可选地记录诊断位置(JSON_DIAGNOSTIC_POSITIONS),调用set_parents()建立父子关系,然后pop_back()弹栈。由此{}结束即代表"该层对象已构造完毕"。

  2. json_sax_dom_callback_parser(带回调的 DOM 构建器)——json_sax.hpp。它在end_object()时把已构造好的整个对象交给用户回调(事件类型parse_event_t::object_end);若回调返回false,该对象会被置为discarded并从父容器中移除。这正是json::parse(json, filter_callback)过滤接口的底层机制。

  3. json_sax_acceptor(只验证合法性)——json_sax.hpp。json::accept()使用它,所有事件一律返回trueend_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()相关的三个关键观察点:

  1. 事件严格嵌套对称Thumbnail对象闭合触发一次end_object()(第 18 行输出),Image对象闭合再触发一次,最外层对象闭合触发第三次。三次end_object()与三处start_object()严格配对,体现了 SAX 事件的括号式流结构——如果每次start_object都要压栈,那么每次end_object就应弹栈。

  2. JSON 文本不携带元素计数start_object(elements=18446744073709551615)中的18446744073709551615正是std::size_t的最大值,即"元素个数未知"的哨兵值(源码中为detail::unknown_size(),见 json_sax.hpp)。如 start_object 文档 所述,只有二进制格式才可能在上报元素数量;JSON 文本解析一律传未知哨兵,所以消费者不能依赖该数值。

  3. end_object()之后才轮到根级收尾与错误处理:示例输入在完整对象闭合后还残留一个]sax_parse(默认strict = true)要求输入被完整消费,于是在三次end_object()之后触发parse_error回调,异常信息为[json.exception.parse_error.101] ... unexpected ']'; expected end of inputparse_error回调返回falsesax_parse随之返回false——这与"返回值为最后一个事件的返回值"完全一致。若该输入末尾没有多余的]sax_parse会以true正常结束。

六、实践要点速览

  • 必须返回boolend_object()的签名必须精确匹配bool end_object(),编译器通过 is_sax.hpp 中的static_assert强制校验;继承json::json_sax_t(json.hpp)可省去逐个核对。
  • 用返回值实现"读到对象边界即停":在end_object()中根据业务条件返回falsesax_parse会立刻停止并返回false;返回true则继续。参考 unit-class_parser.cpp 的SaxCountdown测试类。
  • 嵌套对象按闭合顺序触发:对象结束事件从内到外触发;用于计数时需把end_objectstart_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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 12:31:24

基于OpenBTS 2.8的应急通讯GSM基站搭建实战

简介&#xff1a;一套基于OpenBTS 2.8的GSMS应急通讯系统代码包&#xff0c;面向需自建临时移动通信网络的开发者、应急通信团队及网络技术爱好者&#xff0c;适用于灾害救援、偏远地区信号覆盖和大型活动通信保障等场景。压缩包共2029个文件&#xff0c;大小仅4.45MB&#xff…

作者头像 李华
网站建设 2026/9/9 12:31:18

UG10.0二次开发:uc4650函数与DLL插件开发实战指南

做 UG10.0 二次开发这几年&#xff0c;我一直觉得最容易被新人忽略的&#xff0c;恰恰是那些看起来“老掉牙”的 UC 函数。比如今天要聊的 uc4650&#xff0c;它不是一个多复杂的接口&#xff0c;网上资料也少得可怜&#xff0c;但它在我维护的几个老插件和临时小工具里反复出现…

作者头像 李华
网站建设 2026/9/9 12:28:33

RustDesk 如何用 AppImageBuilder 构建 Linux AppImage 版本?

RustDesk 如何用 AppImageBuilder 构建 Linux AppImage 版本&#xff1f; 【免费下载链接】rustdesk An open-source remote desktop application designed for self-hosting, as an alternative to TeamViewer. 项目地址: https://gitcode.com/GitHub_Trending/ru/rustdesk …

作者头像 李华
网站建设 2026/9/9 12:28:30

架构图设计实战:diagrams.net绘图方法论与工程化最佳实践

前几天给一个老客户的系统做架构评审&#xff0c;对方一边翻PPT一边问我&#xff1a;"你们这套系统的核心调用链&#xff0c;图里怎么没画&#xff1f;"我低头看了两秒&#xff0c;确实没画——不是漏了&#xff0c;是因为那张图我已经画得太乱&#xff0c;根本塞不进…

作者头像 李华
网站建设 2026/9/9 12:27:45

skills:前端AI能力即服务的协议桥接层

1. “skills”不是个名词&#xff0c;而是一套前端开发者正在悄悄迁移的工程范式 最近在几个前端技术群和开源协作频道里&#xff0c;频繁看到有人发类似这样的命令&#xff1a; npx skill add dietrichgebert/ponytail 、 npx skills 、 skills.sh &#xff0c;甚至有人…

作者头像 李华