JsonCpp源码直用:免编译库把JSON解析器集成进C++工程

发布时间:2026/10/15 17:58:13
JsonCpp源码直用:免编译库把JSON解析器集成进C++工程
简介这是一份 JsonCpp 源码包面向希望在 C 工程里直接解析、生成 JSON 数据的开发者核心价值在于省去编译库的配置环节把源码文件放进项目即可调用。压缩包共 38 个文件以 14 个头文件和 5 个 C 源文件为主另有 Visual Studio 工程配置、编译中间文件与调试记录整体仅 1.11MB轻量且目录结构清楚适合快速集成到老版本 VS 项目。资源内头文件与实现源码分离使用者可按需挑选所需部分不必改动既有工程框架附带的工程配置也有助于快速搭建测试环境。已有 961 人学习下载。包内除了核心的 JSON 解析、值对象与写出源码还带有可直接参考的 Visual Studio 工程文件能帮助理解 JsonCpp 的解析与序列化流程遇到编译或链接问题时也可借助工程配置快速核对头文件路径、源文件引用等关键设置对源码级使用 JsonCpp 的 C 开发者比较实用。1. JsonCpp 源码直用绕开编译库把解析器直接塞进项目很多人拿到 JsonCpp 源码的第一反应是找预编译库或者跑一遍 CMake 生成工程但当你面对一个老旧的 C 项目、ARM 交叉编译环境、或者公司内网不允许随意装依赖时“编译库”这条路反而最折腾。JsonCpp 本质是一组 cpp/h 文件构成的解析器源码直用就是把 jsoncpp.cpp 和头文件直接加入你的工程一起编译不生成 dll/lib不引入额外依赖改代码还能直接生效。这份资源适合谁需要在短时间内把 JSON 解析能力集成进既有工程、又不想学整套 CMake 流程的从业者也适合喜欢把第三方代码完全掌握在手里的开发者。接下来我把拆包、接入、调参、踩坑的完整过程复现给你。2. 源码直用前的准备文件结构、版本与编译边界2.1 需要哪些文件JsonCpp 源码树的裁剪清单JsonCpp 仓库拉下来后不要整个目录拖进项目里面有很多测试、文档、CMake 脚本编译时会拖入很多不需要的东西。我通常在 include/json 里保留 json.h、json-forwards.h、json-value.h、json-reader.h、json-writer.h 等头文件以及 src/lib_json/json_reader.cpp、json_value.cpp、json_writer.cpp外加一个关键的 jsoncpp.cpp这是合并后的单文件实现。注意新版 JsonCpp 的源码结构会有差异老版本 0.x 和新版 1.x 头文件分布不同但核心源码文件始终是 reader、value、writer 三兄弟。裁剪后整个源码部分大概 7 个文件非常适合直接挂进 Visual Studio 或 Makefile 工程。为什么不做成 .lib因为很多嵌入式交叉编译工具链对 C 异常、RTTI 的开关和你现有项目不一致预编译库是“黑匣子”一旦某个宏定义不对崩溃了连查的地方都没有。源码直用反而可以把 JSON_CPP_USE_EXCEPTION、JSON_USE_IMPLICIT_CONVERSIONS 这些宏直接放在工程预处理里随时试错。下面是裁剪后的文件清单参考文件用途是否必需include/json/json.h总入口头文件必需include/json/json-forwards.h前向声明减少编译依赖必需include/json/value.hJson::Value 类定义必需include/json/reader.h解析器定义必需include/json/writer.h写入器定义必需src/lib_json/json_reader.cpp解析逻辑实现二选一src/lib_json/json_value.cppValue 存储实现二选一src/lib_json/json_writer.cpp写入逻辑实现二选一jsoncpp.cpp单文件合并版实现二选一如果是从 GitHub 官方仓库下载src/lib_json 下通常没有 jsoncpp.cpp只有三个 cpp 文件此时用三个 cpp 即可。如果下载的是 amalgamated合并版包里面直接有 jsoncpp.cpp 和单个头文件这种更省事。但无论哪种头文件不要从两个地方混用否则可能出现重复声明或版本错位。2.2 先搞懂构建原理编译库和源码直用的本质区别我把这两者的差别简化为“编译时机”和“ABI 绑定”两个点。编译库方式会在项目的 CMake 中先编译 libjsoncpp.so/windows 的静态库然后主工程通过 target_link_libraries 链接而源码直用是把 jsoncpp.cpp 编入主工程的目标文件随后和业务代码一起链接。由于 JsonCpp 的头文件里大量使用 inline 和模板如果封装成动态库头文件版本和库版本出现偏差就会出现链接错误或者运行期访问违例。源码直用彻底消除了这种“版本不一致的 ABI 问题”。你不能把 src/lib_json 下的所有 cpp 一股脑拖进来因为某些版本里已经包含了 jsoncpp.cpp 的全部内容重复编会符号重定义。如果从 GitHub 下载的源码在 src/lib_json 里只有三个 cpp那 jsoncpp.cpp 就是可选的如果下载的是 amalgamated 版本文件叫 jsoncpp.cpp那只需加这一个 cpp 和 include/json 下的头文件。还有一个容易忽略的点JsonCpp 的头文件对 C 标准有硬性要求。新版代码用了 std::unique_ptr、std::move 等 C11 特性如果你的工程还停留在 C98就得换用旧版本源码或者手动补几个智能指针。我遇到过一个项目用 VS2008 编译默认 _MSC_VER 不支持 C11 的某些语法最后降级到 0.10.6 版本源码才通过。所以拿到源码后第一件事是看头文件开头的#if defined(__cplusplus) 判断确认你的编译器能通过。2.3 环境准备与验证先跑通最小读写在加入项目之前先用命令行把源码本身验证一遍。我一般会建一个最小测试目录只放 jsoncpp.cpp、json.h、main.cpp用 g 编译g -stdc11 -I./include main.cpp jsoncpp.cpp -o test_json这里的 -stdc11 是 JsonCpp 新版本的最低要求老版本 0.7.x 用 c03 也能编但 1.9 之后很多函数用到了 override 和 move 语义建议直接开 c11。如果编译失败先看是不是 -I 路径写错或头文件里引用了其它内部头文件。接着最小测试程序#include json/json.h #include iostream int main() { Json::Value root; Json::Reader reader; std::string text {\hello\:1}; bool ok reader.parse(text, root); std::cout ok root[hello].asInt() std::endl; return 0; }parse 的第一个参数是内容第二个是输出根节点返回布尔值表示解析是否成功。这里没有打开异常所以失败不抛异常而是返回 false适合先确认源码能否正常编进程序。如果你要用新版 API也可以这样写#include json/json.h #include sstream #include iostream int main() { std::string text {\hello\:1}; Json::Value root; Json::CharReaderBuilder builder; std::unique_ptrJson::CharReader reader(builder.newCharReader()); std::string errs; const char* begin text.c_str(); const char* end begin text.size(); bool ok reader-parse(begin, end, root, errs); if (ok) { std::cout hello root[hello].asInt() std::endl; } return 0; }为什么用 CharReaderBuilder这是 1.x 推荐的方式它支持通过 builder[collectComments] false 关闭注释收集还能自定义严格模式。而老式 Json::Reader 在新版本中标记为 deprecated但源码里仍然兼容初学者可能会看到两套 API 混搭的代码容易晕。我的建议是既然源码直用就用新版 API以后升级不用改代码。3. 把 JsonCpp 源码塞进你的项目两种主流接入方式3.1 方式一直接把源码文件加入工程这是最简单的路子。拿 Visual Studio 举例在解决方案资源管理器里找到你的项目右键“添加现有项”把 jsoncpp.cpp 和需要的头文件加进来然后在项目的预处理器定义里加上 JSON_USE_IMPLICIT_CONVERSIONS如果要用隐式转换等宏。之后在任何业务 cpp 里写 #include json/json.h 就能用了。使用 Makefile 时把 jsoncpp.cpp 加进 OBJ 列表即可。我习惯单独维护一个 jsoncpp_src 目录避免把第三方源码和业务代码混在一起。比如CXX g CXXFLAGS -stdc11 -Wall -I./third_party/jsoncpp/include SRCS main.cpp network.cpp third_party/jsoncpp/src/lib_json/json_reader.cpp \ third_party/jsoncpp/src/lib_json/json_value.cpp \ third_party/jsoncpp/src/lib_json/json_writer.cpp OBJS $(SRCS:.cpp.o) all: my_app my_app: $(OBJS) $(CXX) $(OBJS) -o $ clean: rm -f $(OBJS) my_app这里注意Makefile 里如果用了旧版本源码可能需要加上 -DJSON_NOEXCEPTION0 之类的宏。CXXFLAGS 中的 -I 一定要指向 include 目录的上一级这样代码里的 #include json/json.h 才有效。如果 -I 指到 include/json那么你的 include 语句得改成 #include json.h但内部头文件之间引用是 json/json-forwards.h这样就会找不到所以最稳妥是 -I/.../include。3.2 方式二用 CMake 的 add_subdirectory 集成如果项目本身就基于 CMake源码直用更规范的做法是把 JsonCpp 源码目录作为子目录加入。前提是你裁剪后的 JsonCpp 目录里有一个最小 CMakeLists.txt或者在主工程里直接用绝对路径。add_library(jsoncpp STATIC ${CMAKE_SOURCE_DIR}/third_party/jsoncpp/src/lib_json/json_reader.cpp ${CMAKE_SOURCE_DIR}/third_party/jsoncpp/src/lib_json/json_value.cpp ${CMAKE_SOURCE_DIR}/third_party/jsoncpp/src/lib_json/json_writer.cpp ) target_include_directories(jsoncpp PUBLIC ${CMAKE_SOURCE_DIR}/third_party/jsoncpp/include ) target_compile_definitions(jsoncpp PUBLIC JSON_USE_IMPLICIT_CONVERSIONS1 )这种方式的好处是把 JsonCpp 隔离成一个目标主工程只要 target_link_libraries(my_app jsoncpp) 即可。但注意 add_subdirectory 是另一条路它是用 JsonCpp 自带的 CMakeLists.txt。如果下载的源码包里有官方 CMakeLists直接在主工程写 add_subdirectory(third_party/jsoncpp) 也行但官方写法会生成 jsoncpp_lib 等目标名字可能和你预想的不同建议先 build 一次看输出。参数说明STATIC 表示静态库但我们目的不是产出 lib而是让 CMake 把它编进主工程PUBLIC 包含路径会对使用方传递。如果官方 CMakeLists 里又开了 BUILD_SHARED_LIBS 选项生成的目标可能是动态库和“源码直用”的初衷相悖。我在实际项目中更倾向自定义一个 add_library不依赖官方 CMake因为官方脚本还会链接 Windows 的 ws2_32 等系统库搞复杂了看着晕。3.3 头文件路径与宏定义不踩 Namespace 坑JsonCpp 的声明全部在 Json 命名空间内头文件包含路径决定你的 include 写法。如果 -I 指向 include 目录那么代码用 #include json/json.h如果直接把 include/json 加进包含路径则用 #include json.h。这两种写法混用最容易被坑因为某些旧版本头文件之间用相对路径互相引用改 include 搜索顺序可能导致找不到 json-forwards.h。宏方面先记两个最常用的JSON_NOEXCEPTION禁用异常解析出错时用 errorMessage() 拿错误信息。这个宏定义后JsonCpp 内部不会抛出异常但你的代码必须检查每个操作是否成功否则遇到非法 JSON 时会出现未定义行为。JSON_USE_IMPLICIT_CONVERSIONS把 Json::Value 和 int/double/bool 做隐式转换如果你的代码经常写 int x root[age]; 就得开这个宏。如果不定义必须用 asInt() 显式转换。还有一个宏 JSON_USE_EXCEPTION老版本里用来开启异常新版本已经改名。我在源码直用时的经验是默认不开 JSON_NOEXCEPTION让异常开着这样访问不存在的 key 时能立刻暴露错误等程序稳定后再考虑关掉。因为 JsonCpp 的 operator[] 如果越界访问异常模式会告诉你哪一行而静默模式可能返回一个默认构造的 Value后续 asInt() 得到 0问题被掩盖。4. 实战用 JsonCpp 源码完成一个配置文件的解析与回写4.1 目标与场景用 JsonCpp 读写程序配置在实际嵌入式项目中JSON 配置常被拿来替代 ini 文件因为支持嵌套。这里我们做一个模拟项目X的配置读写程序需要从 config.json 读取服务器地址、端口、日志等级和重试次数然后修改日志等级后保存回文件。用 JsonCpp 源码直用不依赖任何外部库。这个场景很典型程序启动时读取配置运行中可能修改配置退出前写回。如果使用编译库方式每次构建都要依赖一个 .lib 文件换环境就得重新编库源码直用则把 json_reader.cpp 直接编进主程序交叉编译时只要工具链支持 C11就不会有额外负担。4.2 代码实现解析、修改、保存先定义配置文件内容config.json{ server: 192.168.1.10, port: 8080, log: { level: info, max_size_mb: 16 }, retry: 3 }接下来是解析与回写的完整代码#include json/json.h #include fstream #include iostream #include memory bool loadConfig(const std::string path, Json::Value root) { std::ifstream ifs(path); if (!ifs.is_open()) { std::cerr open failed: path std::endl; return false; } Json::CharReaderBuilder builder; builder[collectComments] false; std::string errs; std::unique_ptrJson::CharReader reader(builder.newCharReader()); bool ok reader-parse( static_caststd::istreambuf_iteratorchar(ifs), static_caststd::istreambuf_iteratorchar(), root, errs); if (!ok) { std::cerr parse err: errs std::endl; } return ok; } bool saveConfig(const std::string path, const Json::Value root) { std::ofstream ofs(path); if (!ofs.is_open()) return false; Json::StreamWriterBuilder builder; builder[indentation] ; std::unique_ptrJson::StreamWriter writer(builder.newStreamWriter()); writer-write(root, ofs); return true; } int main() { Json::Value cfg; if (!loadConfig(config.json, cfg)) return 1; int port cfg[port].asInt(); std::string level cfg[log][level].asString(); std::cout port port log.level level std::endl; cfg[log][level] debug; cfg[retry] 5; if (!saveConfig(config_out.json, cfg)) return 1; return 0; }这里用了 CharReaderBuilder/StreamWriterBuilder 这套新 API而不是老版的 Reader/Writer。原因是从 JsonCpp 1.0 开始官方推荐 builder 模式它允许你通过 builder[属性]值 设置特性比如 collectComments 控制是否把注释收集到 Value 里。parse 使用 istreambuf_iterator 来读取整个流避免了用 FileReadStream 的繁琐。注意 JSONCPP_STRING 是 JsonCpp 内部对 std::string 的宏封装在老版本里可能是 std::string直接写 std::string 也可以但用这个宏兼容自定义字符串类型。builder[indentation] 是设置输出缩进为两个空格。StreamWriterBuilder 还能设置其它属性比如 emitUTF8 用于控制非 ASCII 字符的输出方式默认 true 按 UTF-8 原样输出precision 控制浮点输出精度默认是足够多的有效数字。这些属性都可以通过 builder[key] 传值但必须写对字符串键名写错不会报错只是用默认值。4.3 内存与性能调优点写入缩进与 float 精度保存配置时默认的输出是压缩格式所有人挤一行不方便人工查看。StreamWriterBuilder 的 indentation 可以指定缩进字符串我这里设为两个空格也可以设成 \t。如果要输出紧凑格式直接空字符串。还有一个新手容易踩的精度坑Json::Value 内部保存 double如果原始 JSON 里写 0.10解析后 asDouble 可能变成 0.1回写时 rtValue 会把 0.1 写成 0.1 而非 0.10这对金额或科学计算可能无法接受。解决方法是回写前用自定义类型或字符串保存原始值或者在 Value 里存 string 而不是 double。在实际项目中我经常看到有人把整型也存成 double然后在 asInt 时发现精度丢失。JsonCpp 的 Value 类型有 int、uint、int64、uint64、double 几种写入时如果 root[port] 8080; 默认按 int 保存但如果你先写了一个 double 再改 int可能类型混乱。最好的做法是构造值的时候显式调用 Json::Value(8080) 或直接赋值避免混合运算导致类型变化。5. 避坑与常见问题源码直用时的五个血泪点5.1 坑一符号重复定义——多文件加入导致的链接错误现象链接期报错大量 “json_value.cpp.obj : error LNK2005: xxx already defined in jsoncpp.cpp.obj” 或者 g 下多个重复符号。原因同一份源码既有 jsoncpp.cpp 又把 lib_json 下的三个 cpp 全部加入工程导致同一个类的实现被编译了两次。有些下载包版本同时提供单文件版和分离版新手容易两个都加。解决检查工程项目里是否同时包含 jsoncpp.cpp 和 src/lib_json/*.cpp只保留一组。如果是从 GitHub 主仓库拉的源码src/lib_json 下本身没有 jsoncpp.cpp这种直接用三个 cpp 即可。如果是从 amalgamated 包下载只有 jsoncpp.cpp那就用它不要再去单独加三个 cpp。我一般会在工程文件里搜索 “json_reader.cpp”如果发现有两处路径不同就要立刻停下来确认。5.2 坑二版本不一致——新旧 API 混用现象代码里用了 Json::Reader 和 Json::Writer编译通过但运行时行为怪或者用 json/autolink.h 却找不到符号。原因不同版本 JsonCpp 的 API 差异很大。0.6 时代没有 CharReaderBuilder1.9 时代又把 Reader 标记为 deprecated。如果从网上随手下的示例代码用了新 API而源码包是旧版直接翻车。解决统一版本。我一般用 1.9.x 源码包并把代码里所有 Reader/Writer 换成 CharReaderBuilder/StreamWriterBuilder。如果必须用老 API就在 include 之前定义宏 JSONCPP_DEPRECATED但这不是长久之计最好跟着新版改。换版本时还要注意头文件里是否增加了新的宏定义比如 JSONCPP_OP_EXPLICIT这个宏在不同版本里控制 explicit 关键字老编译器不认识会报错。5.3 坑三大小写敏感的头文件路径现象在 Windows 上编译没问题切到 Linux 后报错 “fatal error: json/JSON.h: No such file or directory”或者在某些旧工程里 include “json/value.h” 说找不到。原因Windows 文件系统不区分大小写而 Linux 区分。JsonCpp 官方头文件是 json/json.h全部小写但很多教程写 json/Json.h或者项目里缓存了旧头文件名。解决统一头文件路径为小写。且在剪裁源码时保留 include/json 目录下的原始文件名不要复制到别的目录后改扩展名。我在项目里一般会全局搜索 #include json/ 确认全部是小写。还有一个细节有些版本已经把头文件从 include/json 移动到了 include/jsoncpp/json如果你下载的资源来自某个特定整合包include 路径可能需要写 jsoncpp/json/json.h这时候不要死板地坚持 json/。5.4 坑四中文编码与 UTF-8 BOM现象解析含有中文的 JSON 文件输出乱码或者在 Windows 下用记事本编辑保存后jsoncpp 解析报错。原因文件保存成了 GBK或者带 BOM 的 UTF-8。JsonCpp 解析器按 UTF-8 处理GBK 中文会被当作非法的 UTF-8 序列。解决用 Notepad 或 VS Code 把文件转成 UTF-8 无 BOM并且程序读入时统一按 UTF-8 处理。如果必须在运行时处理 GBK需要先转码再传给 JsonCpp。另外Visual Studio 的编译器在处理字符串字面量时存在本地代码页问题在代码里写 std::string s 中文; 最终 s 的编码取决于源文件保存编码建议源文件统一保存成带签名 UTF-8 或纯 UTF-8。解析出错时的错误信息位置很重要。我用 CharReaderBuilder 时parse 失败后 errs 里会包含行号和列号比如 line 1:0: undocumented token。这个 errs 可能是一长串字符串但实际有用的就第一行。我调试时习惯把 errs 完整打出来不要只打 errs.c_str()因为内部可能有多个错误累积。5.5 坑五异常开关——禁用异常后怎么处理现象定义 JSON_NOEXCEPTION 后解析格式错误时程序崩溃或静默失败找不到错误信息。原因JsonCpp 内部很多地方依赖异常来报告错误比如 Value::operator[] 访问不存在的 key 时默认会抛异常。如果关掉了异常它只能走 assert 或返回空值行为改变。解决首选不定义 JSON_NOEXCEPTION一直启用异常在嵌入式环境实在不能用异常时代码里必须提前用 isMember() 或 isNull() 检查 key 是否存在并用 parse 的返回值和 errorMessage() 处理解析错误。我的习惯是用 isMember 检查所有必要字段否则程序直接返回退出绝不能假设 key 一定存在。举个例子配置文件中如果 server 字段缺失直接访问 cfg[server] 会得到一个 null ValueasString() 返回空字符串后续连接时可能莫名其妙失败。更好的写法是if (!cfg.isMember(server) || !cfg[server].isString()) { std::cerr config missing server std::endl; return 1; }isMember 和 isString 是两类不同检查isMember 判断 key 是否存在isString 判断值类型。有些新手只用 isMember但 true 对应的可能是数字类型asString 一样会得到空串必须两个都验。6. 进阶技巧把 JsonCpp 源码封装成自己的工具模块把第三方源码直接编进项目后下一步就是减少业务代码对 JsonCpp API 的直接依赖。我一般会再封装一个 util 头文件让所有 Json 操作走自己的薄接口。6.1 封装一个简单的 Json 工具类// json_util.h #pragma once #include json/json.h #include string #include optional namespace app { bool parseJson(const std::string text, Json::Value root); std::optionalstd::string getString(const Json::Value root, const std::string key); int getInt(const Json::Value root, const std::string key, int def); std::string dumpJson(const Json::Value root, bool pretty); }实现里用 CharReaderBuilder 解析、StreamWriterBuilder 输出这样业务层永远只面对 std::string 和处理好的 Value。遇到缺字段时 optional 返回空业务侧不再关心抛异常的细节。这个封装对团队协作很有用把 ConfigManager 类的一次性解析逻辑集中在 load 函数里后续配置文件格式变化只改一处。具体实现时parseJson 只需要几行bool parseJson(const std::string text, Json::Value root) { Json::CharReaderBuilder builder; std::unique_ptrJson::CharReader reader(builder.newCharReader()); std::string errs; return reader-parse(text.data(), text.data() text.size(), root, errs); }这段代码里我没有把 errs 返回出去如果调用方想知道失败原因可以重载一个带 errs 参数的版本。dumpJson 里则通过 pretty 参数切换缩进std::string dumpJson(const Json::Value root, bool pretty) { Json::StreamWriterBuilder builder; builder[indentation] pretty ? : ; std::ostringstream oss; std::unique_ptrJson::StreamWriter writer(builder.newStreamWriter()); writer-write(root, oss); return oss.str(); }注意 string compaction 模式下indentation 设为 即可。StreamWriter 默认会在写完最后一个字符后输出换行如果你不需要末尾换行可以在写入后把字符串的末尾 \n 去掉。6.2 快速验证脚本与后续维护习惯拿到任何 JsonCpp 源码包我建议先做一个 10 分钟可跑的 smoke test就是第 2 章那个最小程序。把它放在 demo 目录编译运行确认输出符合预期后再去干别的。之后每次把源码升级不敢偷懒强制走一遍这个验证再跑到项目里跑回归。从那以后我每次裁剪源码和换版本都会执行一遍这个流程有效避免了新版本 API 迁移带来的隐性问题。希望帮到你。本文还有配套的精品资源点击获取