📚 系列:C++ 工程实践 第 04 篇 ⏱ 阅读约 12 分钟
📌 一句话总结
本文用「解析 → 取值 → 生成 → 踩坑 」四步讲透 jsoncpp,读完后你能:① 5 分钟跑通 C++ 读写 JSON;② 用 Json::Value 安全访问嵌套对象与数组;③ 避开 int64、浮点精度、注释、性能四个高频坑。
文章目录
-
- [1. 为什么还需要 jsoncpp:老而稳的 JSON 库](#1. 为什么还需要 jsoncpp:老而稳的 JSON 库)
- [2. 安装与最小工程:5 分钟跑通](#2. 安装与最小工程:5 分钟跑通)
- [3. 解析 JSON:从字符串、文件到流](#3. 解析 JSON:从字符串、文件到流)
- [4. 访问数据:Json::Value 的类型与遍历](#4. 访问数据:Json::Value 的类型与遍历)
- [5. 生成 JSON:从 Value 到字符串](#5. 生成 JSON:从 Value 到字符串)
- [6. 实战:配置读取 + 接口报文组装](#6. 实战:配置读取 + 接口报文组装)
- [7. 四个高频坑:整数、精度、注释、性能](#7. 四个高频坑:整数、精度、注释、性能)
- [8. 总结与下一步](#8. 总结与下一步)
1. 为什么还需要 jsoncpp:老而稳的 JSON 库
先说结论:jsoncpp 不是最快的 JSON 库,但 API 直白、发行版打包成熟,适合老项目和「先跑通再说」的场景。
C++ 里能读写 JSON 的库不少,老项目里却常常还是 jsoncpp:它早早进了各大发行版的官方仓库,一条 apt 命令就能装,入口头文件只有一个,不依赖 Boost,C++11 就能编译。项目目前处于维护模式,新特性不多,但修复和发版仍在继续,对求稳的服务反而是好事。
| 库 | 依赖 | 性能 | 适合场景 |
|---|---|---|---|
| jsoncpp | 无 | 中 | 老项目、配置文件、上手快 |
| nlohmann/json | header-only | 中 | 现代 C++、喜欢链式语法 |
| RapidJSON | header-only | 高 | 高吞吐解析、热路径 |
| Boost.PropertyTree | Boost | 低 | 已重度使用 Boost 的项目 |
一句话选型:中小报文、配置文件、维护中的老服务用 jsoncpp;百万级 QPS 的纯解析场景,再考虑 RapidJSON。
2. 安装与最小工程:5 分钟跑通
先说结论:装库只是第一步,大部分「jsoncpp 用不了」都卡在链接参数和头文件路径上。
按项目现状选安装方式:
| 系统 / 方式 | 命令 |
|---|---|
| Ubuntu / Debian | sudo apt install libjsoncpp-dev |
| vcpkg | vcpkg install jsoncpp |
| 源码 / FetchContent | 用 CMake 编译安装,或直接把源码加进工程 |
源码方式适合要锁定版本、甚至要改源码的项目:用 FetchContent 把 target 直接带进工程,比手动装到系统目录更好复现。
装完先确认编译参数能拿到:
📄 终端 · 装包并验证 pkg-config
bash
# Ubuntu / Debian:装开发包(含头文件、库、pkg-config 描述)
sudo apt install -y libjsoncpp-dev
# 能打印出版本号,说明装好了
pkg-config --modversion jsoncpp
这时再写一个最小程序,最常见的失败长这样:
⚠️ 最经典的报错
编译能过、链接失败,报 undefined reference to 'Json::Value::Value(Json::ValueType)'。这不是代码写错了,是链接时没找到 jsoncpp 库,按下面三种方式修。
- CMake 工程:
find_package(jsoncpp CONFIG REQUIRED),链接JsonCpp::JsonCpp(1.9 及之后的发行版包); - 老一点的包只导出
jsoncpp_lib或jsoncpp_static,换目标名即可; - 手写 g++:
-I/usr/include/jsoncpp -ljsoncpp,链接参数必须放在源文件之后,放到前面照样报 undefined reference。
CMake 工程里推荐用 config 模式查找:
📄 CMakeLists.txt · 关键在导入目标名
cmake
cmake_minimum_required(VERSION 3.16)
project(json_demo CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# 1.9 及之后的发行版包一般导出 JsonCpp::JsonCpp
find_package(jsoncpp CONFIG REQUIRED)
add_executable(json_demo main.cc)
target_link_libraries(json_demo PRIVATE JsonCpp::JsonCpp)
先写 main.cc(第 3 节会逐行拆;写完再回来执行下面的编译命令):
📄 main.cc · 最小可用:字符串解析
cpp
// main.cc
#include <json/json.h>
#include <iostream>
#include <memory>
#include <string>
int main() {
const std::string text = R"({"name":"alice","age":18,"vip":true})";
Json::CharReaderBuilder builder; // ① 解析器工厂
std::unique_ptr<Json::CharReader> reader(
builder.newCharReader()); // ② 每次解析建一个 reader
Json::Value root;
std::string errs;
// ③ 传 [begin, end) 指针区间,错误信息由 errs 带出
bool ok = reader->parse(text.data(), text.data() + text.size(),
&root, &errs);
if (!ok) {
std::cerr << "parse failed: " << errs << std::endl;
return 1;
}
std::cout << root["name"].asString() << " age="
<< root["age"].asInt() << " vip="
<< root["vip"].asBool() << std::endl;
return 0;
}
跑起来输出:
text
alice age=18 vip=1
不用 CMake 时,pkg-config 最省事:
📄 终端 · 两条命令等价
bash
# pkg-config 自动带上 -I/usr/include/jsoncpp 和 -ljsoncpp
g++ -std=c++17 main.cc -o json_demo $(pkg-config --cflags --libs jsoncpp)
# 等价的手写形式;-ljsoncpp 必须放在源文件之后
g++ -std=c++17 main.cc -o json_demo -I/usr/include/jsoncpp -ljsoncpp
ℹ️ 头文件为什么在 jsoncpp 目录下
Debian / Ubuntu 的包把头文件装在 /usr/include/jsoncpp 下,所以源码里写 #include <json/json.h> 时,编译命令必须带 -I/usr/include/jsoncpp。直接抄网上不带 -I 的命令,就会死在「找不到 json/json.h」。
3. 解析 JSON:从字符串、文件到流
先说结论:解析只有一条主线:CharReaderBuilder 造 reader,parse 返回 bool,错误信息在 errs 里。 上一节已经跑通最小版本,这里补三个容易踩的细节。
三个要点:传入的是 [begin, end) 指针区间,字符串不必以 \0 结尾;reader 用完即弃,不要跨线程共享同一个 reader;解析失败时 root 的内容不可信,必须立刻返回。
文件解析用 Json::parseFromStream 更省事,它直接吃一个 std::istream:
📄 file.cc · 文件解析的错误处理
cpp
// file.cc
#include <json/json.h>
#include <fstream>
#include <iostream>
#include <string>
int main() {
std::ifstream ifs("config.json"); // ① 先看文件是否打开
if (!ifs) {
std::cerr << "open config.json failed" << std::endl;
return 1;
}
Json::CharReaderBuilder builder;
Json::Value root;
std::string errs;
// ② parseFromStream 直接吃 std::istream,失败时 errs 里有行列号
if (!Json::parseFromStream(builder, ifs, &root, &errs)) {
std::cerr << "parse config failed: " << errs << std::endl;
return 1;
}
std::cout << "port=" << root["port"].asInt() << std::endl;
return 0;
}
失败时 errs 的格式是「* Line 行号, Column 列号」加一行错误描述,比如缺少值时打印 * Line 4, Column 1 和 Syntax error: value, object or array expected.。把 errs 原样打日志,比只打一句「解析失败」有用得多。流解析同理:把字符串塞进 std::istringstream 再调 parseFromStream,就能复用同一套错误处理。
✅ 最佳实践
把「解析 + 校验」封装成一个返回错误信息的函数,调用方只处理成功和失败两条路径;不要在业务代码里散落 reader->parse 和裸 try/catch。
4. 访问数据:Json::Value 的类型与遍历
先说结论:Json::Value 没有「万能取值」,正确姿势是先判类型、再取值,缺字段用 get 给默认值。 反过来,类型不对时 asString() / asInt() 不会悄悄返回空串或 0,而是抛 Json::LogicError(Type is not convertible to ...;关闭异常时直接 abort)------线上 root["port"].asString() 崩掉,多半就是漏了这一步判断。
类型判断和取值是一一对应的:
| 判断方法 | 对应 JSON | 取值方法 |
|---|---|---|
isNull |
null | 无 |
isBool |
true / false | asBool |
isInt / isInt64 |
整数 | asInt / asInt64 |
isUInt64 |
无符号整数 | asUInt64 |
isDouble |
小数 | asDouble |
isString |
字符串 | asString |
isArray / isObject |
数组 / 对象 | size / 下标 / get |
嵌套对象和数组这样访问:
📄 access.cc · 判类型 → get / 遍历
cpp
// access.cc
#include <json/json.h>
#include <iostream>
#include <memory>
#include <string>
int main() {
const std::string text = R"({
"service": "order",
"tags": ["prod", "v2"],
"db": {"host": "127.0.0.1", "port": 3306}
})";
Json::CharReaderBuilder builder;
std::unique_ptr<Json::CharReader> reader(builder.newCharReader());
Json::Value root;
std::string errs;
if (!reader->parse(text.data(), text.data() + text.size(), &root, &errs))
return 1;
// ① 嵌套对象:先 isMember 再 get,get 的第二个参数是缺省值
if (root.isMember("db") && root["db"].isObject()) {
const Json::Value& db = root["db"]; // 已判存在,[] 取值安全
std::cout << db.get("host", "localhost").asString() << ":"
<< db.get("port", 3306).asInt() << std::endl;
}
// ② 数组:范围 for,元素是 Json::Value
for (const Json::Value& tag : root["tags"]) // 只读已有元素,安全
std::cout << tag.asString() << " ";
std::cout << std::endl;
// ③ 对象:getMemberNames() 拿到 key,再按下标取值
for (const std::string& key : root.getMemberNames()) // key 都来自已有成员,安全
std::cout << key << " isObject=" << root[key].isObject() << std::endl;
return 0;
}
最经典的坑是 operator[]:在非 const 的 Json::Value 上读一个不存在的 key,它会顺手插入一个 null 成员 。你本意是「检查字段在不在」,结果反而把字段创建了出来,序列化时就多出 "timeout": null。
📄 operator_bracket.cc · 错误写法 vs 正确写法
cpp
// operator_bracket.cc
#include <json/json.h>
#include <iostream>
int main() {
Json::Value conf; // 空对象
// 错误写法:非 const 的 operator[] 在 key 不存在时会插入 null 成员
if (conf["timeout"].isNull()) {
std::cout << "timeout missing" << std::endl;
}
std::cout << "has timeout=" << conf.isMember("timeout") // ① 已经变成 true
<< " size=" << conf.size() << std::endl;
// 正确写法:const 引用 + get + 缺省值,只读不写
const Json::Value& ro = conf;
const int timeout = ro.get("timeout", 3000).asInt();
std::cout << "timeout=" << timeout
<< " has timeout=" << conf.isMember("timeout") << std::endl;
return 0;
}
另外,asInt() 对超出 int 范围的整数怎么截断,不同版本行为并不一致(待实测),涉及 13 位以上整数统一用 asInt64()。
5. 生成 JSON:从 Value 到字符串
先说结论:先拼 Json::Value,再交给 StreamWriterBuilder 输出;缩进、精度、UTF-8 三个参数决定输出长什么样。
📄 build.cc · 构造 + 序列化
cpp
// build.cc
#include <json/json.h>
#include <iostream>
int main() {
// ① 先定容器类型,再填内容
Json::Value root(Json::objectValue);
root["service"] = "order";
root["port"] = 8080;
root["debug"] = false;
Json::Value tags(Json::arrayValue); // ② 数组先 append 再挂上去
tags.append("prod");
tags.append("v2");
root["tags"] = tags;
Json::Value db(Json::objectValue);
db["host"] = "127.0.0.1";
root["db"] = db;
// ③ StreamWriterBuilder 控制输出格式
Json::StreamWriterBuilder builder;
builder["indentation"] = " "; // "" 即紧凑单行
builder["precision"] = 15; // 有效数字位数
builder["emitUTF8"] = true; // 中文原样输出,不做 \u 转义
const std::string out = Json::writeString(builder, root);
std::cout << out << std::endl;
return 0;
}
输出(缩进 2 空格、中文原样、精度可控):
text
{
"service" : "order",
"port" : 8080,
"debug" : false,
"tags" : [ "prod", "v2" ],
"db" : {
"host" : "127.0.0.1"
}
}
三种输出方式,按场景选:
| 方式 | 写法 | 何时用 |
|---|---|---|
StreamWriterBuilder |
Json::writeString(builder, v) |
接口报文、落盘,默认选它 |
toStyledString() |
v.toStyledString() |
本地调试、打日志 |
FastWriter |
Json::FastWriter().write(v) |
认老代码时知道它是紧凑输出 |
toStyledString() 用的是内置 StyledWriter,缩进固定 3 空格、不能配置;FastWriter 是更老的 API,输出紧凑单行。要统一格式,还是用 StreamWriterBuilder。它的常用参数(1.9.x 默认值):indentation 为 \t、precision 为 17、emitUTF8 为 false、commentStyle 为 All。精度不显式设置时,0.1 会被写成 0.10000000000000001。
6. 实战:配置读取 + 接口报文组装
先说结论:实战里 JSON 处理就两件事:入口把外部 JSON 校验成可信结构,出口把内部结构组装成稳定报文。
数据流很直白:
#mermaid-svg-aG5JzQKV8EUScFUE{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-aG5JzQKV8EUScFUE .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-aG5JzQKV8EUScFUE .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-aG5JzQKV8EUScFUE .error-icon{fill:#552222;}#mermaid-svg-aG5JzQKV8EUScFUE .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-aG5JzQKV8EUScFUE .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-aG5JzQKV8EUScFUE .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-aG5JzQKV8EUScFUE .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-aG5JzQKV8EUScFUE .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-aG5JzQKV8EUScFUE .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-aG5JzQKV8EUScFUE .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-aG5JzQKV8EUScFUE .marker{fill:#333333;stroke:#333333;}#mermaid-svg-aG5JzQKV8EUScFUE .marker.cross{stroke:#333333;}#mermaid-svg-aG5JzQKV8EUScFUE svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-aG5JzQKV8EUScFUE p{margin:0;}#mermaid-svg-aG5JzQKV8EUScFUE .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-aG5JzQKV8EUScFUE .cluster-label text{fill:#333;}#mermaid-svg-aG5JzQKV8EUScFUE .cluster-label span{color:#333;}#mermaid-svg-aG5JzQKV8EUScFUE .cluster-label span p{background-color:transparent;}#mermaid-svg-aG5JzQKV8EUScFUE .label text,#mermaid-svg-aG5JzQKV8EUScFUE span{fill:#333;color:#333;}#mermaid-svg-aG5JzQKV8EUScFUE .node rect,#mermaid-svg-aG5JzQKV8EUScFUE .node circle,#mermaid-svg-aG5JzQKV8EUScFUE .node ellipse,#mermaid-svg-aG5JzQKV8EUScFUE .node polygon,#mermaid-svg-aG5JzQKV8EUScFUE .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-aG5JzQKV8EUScFUE .rough-node .label text,#mermaid-svg-aG5JzQKV8EUScFUE .node .label text,#mermaid-svg-aG5JzQKV8EUScFUE .image-shape .label,#mermaid-svg-aG5JzQKV8EUScFUE .icon-shape .label{text-anchor:middle;}#mermaid-svg-aG5JzQKV8EUScFUE .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-aG5JzQKV8EUScFUE .rough-node .label,#mermaid-svg-aG5JzQKV8EUScFUE .node .label,#mermaid-svg-aG5JzQKV8EUScFUE .image-shape .label,#mermaid-svg-aG5JzQKV8EUScFUE .icon-shape .label{text-align:center;}#mermaid-svg-aG5JzQKV8EUScFUE .node.clickable{cursor:pointer;}#mermaid-svg-aG5JzQKV8EUScFUE .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-aG5JzQKV8EUScFUE .arrowheadPath{fill:#333333;}#mermaid-svg-aG5JzQKV8EUScFUE .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-aG5JzQKV8EUScFUE .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-aG5JzQKV8EUScFUE .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-aG5JzQKV8EUScFUE .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-aG5JzQKV8EUScFUE .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-aG5JzQKV8EUScFUE .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-aG5JzQKV8EUScFUE .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-aG5JzQKV8EUScFUE .cluster text{fill:#333;}#mermaid-svg-aG5JzQKV8EUScFUE .cluster span{color:#333;}#mermaid-svg-aG5JzQKV8EUScFUE div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-aG5JzQKV8EUScFUE .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-aG5JzQKV8EUScFUE rect.text{fill:none;stroke-width:0;}#mermaid-svg-aG5JzQKV8EUScFUE .icon-shape,#mermaid-svg-aG5JzQKV8EUScFUE .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-aG5JzQKV8EUScFUE .icon-shape p,#mermaid-svg-aG5JzQKV8EUScFUE .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-aG5JzQKV8EUScFUE .icon-shape .label rect,#mermaid-svg-aG5JzQKV8EUScFUE .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-aG5JzQKV8EUScFUE .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-aG5JzQKV8EUScFUE .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-aG5JzQKV8EUScFUE :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 通过
config.json
parseFromStream
Json::Value
字段校验
组装响应 Value
writeString
response.json
读配置并校验必填字段:
📄 config.cc · 语法错误和结构错误分开报
cpp
// config.cc
#include <json/json.h>
#include <fstream>
#include <iostream>
#include <string>
struct Config {
std::string host;
int port = 0;
int timeout_ms = 3000;
bool debug = false;
};
bool LoadConfig(const std::string& path, Config* cfg, std::string* err) {
std::ifstream ifs(path);
if (!ifs) { *err = "open " + path + " failed"; return false; }
Json::CharReaderBuilder builder;
Json::Value root;
std::string errs;
if (!Json::parseFromStream(builder, ifs, &root, &errs)) { // ① 语法层
*err = "syntax: " + errs;
return false;
}
if (!root.isObject()) { *err = "root must be object"; return false; }
// ② 结构层:必填字段存在且类型正确
if (!root.isMember("host") || !root["host"].isString() ||
!root.isMember("port") || !root["port"].isInt()) {
*err = "host/port missing or wrong type";
return false;
}
cfg->host = root["host"].asString();
cfg->port = root["port"].asInt();
cfg->timeout_ms = root.get("timeout_ms", 3000).asInt(); // ③ 可选字段给缺省
cfg->debug = root.get("debug", false).asBool();
return true;
}
int main() {
Config cfg;
std::string err;
if (!LoadConfig("config.json", &cfg, &err)) {
std::cerr << "load config failed: " << err << std::endl;
return 1;
}
std::cout << cfg.host << ":" << cfg.port
<< " timeout=" << cfg.timeout_ms << std::endl;
return 0;
}
组报文并落盘:
📄 response.cc · 紧凑输出 + 写文件
cpp
// response.cc
#include <json/json.h>
#include <fstream>
#include <iostream>
int main() {
// 假设 req 是从上游解析出来的请求
Json::Value req;
req["order_id"] = "A1001";
req["items"].append("book");
req["items"].append("pen");
Json::Value resp(Json::objectValue); // ① 响应固定为对象
resp["code"] = 0;
resp["msg"] = "ok";
resp["data"]["order_id"] = req["order_id"];
resp["data"]["item_count"] =
static_cast<Json::UInt>(req["items"].size());
resp["data"]["items"] = req["items"];
Json::StreamWriterBuilder builder;
builder["indentation"] = ""; // ② 接口报文紧凑输出
const std::string body = Json::writeString(builder, resp);
std::ofstream ofs("response.json"); // ③ 落盘
ofs << body;
std::cout << body << std::endl;
return 0;
}
紧凑输出(indentation 设为空字符串):
text
{"code":0,"msg":"ok","data":{"order_id":"A1001","item_count":2,"items":["book","pen"]}}
工程上再补三点:语法错误和结构错误分开报,日志里才定位得动;可选字段一律在加载函数里给缺省值,主流程不重复判空;输出封装成一个函数,缩进、精度、UTF-8 只在一处设置,否则同一个项目里会出现三种格式。另外,配置加载失败要在启动阶段直接退出,别带着空 host 继续跑;接口报文则相反,失败也要返回结构化的错误码和消息。
7. 四个高频坑:整数、精度、注释、性能
先说结论:四类坑的共同点是「类型和预期错位」:你以为它是 int,其实是 int64;你以为注释会跟着走,其实不会。
| 坑 | 现象 | 修法 |
|---|---|---|
| int64 截断 | 13 位时间戳读成负数 | isInt64 + asInt64 |
| 浮点精度 | 0.1 输出成 0.10000000000000001 | 显式设 precision,别用 == |
| 注释保留 | 跨语言后注释丢失或报错 | 注释只给配置文件用 |
| 性能 | 热路径反复解析拖慢 | 缓存解析结果,或换库 |
📄 pitfalls.cc · int64、浮点精度与中文编码
cpp
// pitfalls.cc
#include <json/json.h>
#include <iomanip>
#include <iostream>
#include <memory>
#include <string>
int main() {
// 13 位毫秒时间戳,int 装不下;金额用浮点表示会丢精度
const std::string text = R"({"ts": 1699999999999, "amount": 0.1})";
Json::CharReaderBuilder builder;
std::unique_ptr<Json::CharReader> reader(builder.newCharReader());
Json::Value root;
std::string errs;
if (!reader->parse(text.data(), text.data() + text.size(), &root, &errs))
return 1;
// ① 64 位整数:判断 isInt64,取值用 asInt64
std::cout << "isInt64=" << root["ts"].isInt64()
<< " ts=" << root["ts"].asInt64() << std::endl;
// ② 浮点:用 asDouble 取值,输出精度由 writer 的 precision 决定
const double amount = root["amount"].asDouble();
std::cout << std::setprecision(17) << "amount=" << amount << std::endl;
Json::StreamWriterBuilder wb;
wb["precision"] = 15;
std::cout << "json=" << Json::writeString(wb, root["amount"]) << std::endl;
// ③ 中文:默认 emitUTF8=false,输出的是 \u 转义
Json::Value zh("订单创建成功");
Json::StreamWriterBuilder zb;
std::cout << "default=" << Json::writeString(zb, zh) << std::endl;
zb["emitUTF8"] = true;
std::cout << "utf8=" << Json::writeString(zb, zh) << std::endl;
return 0;
}
注释要单独说:jsoncpp 的 allowComments / collectComments 默认是开的,读进来的注释会挂在 Value 上,StreamWriterBuilder 默认 commentStyle 为 All 也会写回。但这只是 jsoncpp 的扩展------JSON 标准本身不允许注释,其他语言或服务收到 // 会直接解析失败。所以注释只用在「人读的配置文件」里,接口报文一个都不要留;而且注释跟 Value 绑定,重新构造的新 Value 不会带过去。需要严格模式时用 CharReaderBuilder::strictMode(&settings),它会把 allowComments 关掉。
还有三个容易忽略的点:
Members的顺序 :getMemberNames()底层是std::map,按 key 字典序返回,不是插入顺序。联调时字段顺序变了不代表数据丢了;需要保序就把有序数据放进数组。- 中文编码 :
emitUTF8默认 false,中文会被转成\u开头的转义序列,看起来像乱码。看到满屏\u8ba2先检查这个参数(1.9 及之后才有)。读取端只认 UTF-8:Windows 下 GBK 编码的配置文件要先转码,否则中文解析出来是乱码甚至直接失败。 - 写入 64 位整数 :
root["ts"] = Json::Int64(1699999999999);超过2^63-1用Json::UInt64;再大就会退化成 double 丢精度,金额、雪花 ID 别直接塞 double。 - 修改与删除 :对象删字段用
root.removeMember("k");数组改元素用arr[i] = v、删元素用arr.removeIndex(i),没有erase这类 STL 风格接口。 - 重复 key 与多余内容 :
rejectDupKeys为 false 时重复 key 静默覆盖,failIfExtra为 false 时 JSON 后面多一段内容也不报错。外部输入建议显式打开这两个开关,代价是与旧数据的兼容性变差;1.9.x 的 CharReaderBuilder 默认表里这两项都在,更老的版本以你的头文件为准。
最后说性能:jsoncpp 的单次解析比 RapidJSON 慢几倍(倍数随数据形态和版本变化),但绝大多数业务根本到不了这个瓶颈。真正要避的是在热路径里反复解析同一段 JSON ------把解析结果缓存成 Json::Value 或结构体,比换库的收益大得多。
8. 总结与下一步
先说结论:jsoncpp 够用且稳定,前提是知道它的边界:注释是扩展、精度要显式设置、性能不是它的强项。
选型上,老项目别为了换库而换库;新项目追求语法糖可以看 nlohmann/json,追求极致解析性能看 RapidJSON,跨语言、跨团队的契约用 protobuf。jsoncpp 的位置很清楚:中小报文、配置文件、稳定优先。
📝 小结
① 解析 :CharReaderBuilder + parse,失败先看 errs;文件用 parseFromStream。
② 取值 :先判类型再取值,缺省用 get;别用非 const 的 operator[] 去试探 key。
③ 输出与坑 :StreamWriterBuilder 统一设缩进、精度、emitUTF8;int64 用 asInt64,浮点别用 ==。
🔜 下一篇
《protobuf 实战:从 .proto 到 C++ 序列化的完整链路》------把结构化数据从 JSON 升级到二进制协议,看看契约、兼容性和性能分别换来什么。关注专栏不迷路。
🔗 参考
1 jsoncpp GitHub 仓库:https://github.com/open-source-parsers/jsoncpp
2 jsoncpp 默认配置源码(1.9.5):json_write
3 pkg-config 用法:https://www.freedesktop.org/wiki/Software/pkg-config/
4 C++ 微服务脚手架(VOD 系统)项目:cpp-microservice-scaffold-vod-system
-config/