jsoncpp 使用指南:编译链接、Json::Value 取值与高频坑

📚 系列: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 库,按下面三种方式修。

  1. CMake 工程:find_package(jsoncpp CONFIG REQUIRED),链接 JsonCpp::JsonCpp(1.9 及之后的发行版包);
  2. 老一点的包只导出 jsoncpp_lib 或 jsoncpp_static,换目标名即可;
  3. 手写 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/

相关推荐
布莱克6051 小时前
理解 = delete 与 = default:两种赋值语义的差异
c++
小小、码农2 小时前
[特殊字符]【C++11】类的新功能全面解析:移动构造、移动赋值、default、delete、final 与 override
linux·开发语言·网络·c++
库玛西3 小时前
外部排序全解析:败者树、置换-选择排序与最佳归并树
c语言·数据结构·c++·笔记·考研·算法
General_G5 小时前
创建型模式—原型(Prototype)模式
c++·设计模式·原型模式
syagain_zsx5 小时前
protobuf 实战:从 .proto 到 C++ 序列化的完整链路
c++·学习·程序人生·谷歌·protobuf
welfare9625 小时前
新号别搞:C++_list
开发语言·c++
数据狐(Datafox)5 小时前
一文读懂京东商品详情 API:返回字段解读与 JSON 样例
开发语言·数据库·爬虫·json
xiaoye-duck5 小时前
《C++ 项目:手写大模型接入 SDK》云端 API 调用基础与 Apifox 调试实战
网络·c++·语言模型
坤坤子吖6 小时前
Linux 线程同步与互斥——条件变量与线程同步
linux·开发语言·c++·笔记·学习