ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

《C++ 工具库实战》C++ 解析利器 JsonCpp:从基础使用到封装简易 JSON 工具

《C++ 工具库实战》C++ 解析利器 JsonCpp:从基础使用到封装简易 JSON 工具

🔥小叶-duck:个人主页

❄️个人专栏:《Data-Structure-Learning》《C++入门到进阶&自我学习过程记录》
《Linux系统从入门到实践》《Linux网络从入门到实践》
《Qt 方寸极境》 《MySQL》

✨未择之路,不须回头
已择之路,纵是荆棘遍野,亦作花海遨游


目录

前言

一、Json 简介

1.1 基本结构

1.2 语法规则

1.3 演示示例

1.4 用途

二、JsonCpp 介绍

2.1 JsonCpp 安装

​编辑

三、JsonCpp 使用

3.1 包含头文件

3.2 链接库

3.3 Json::Value 类型枚举

3.4 中间数据转储类 Json::Value 常用 API

3.4.1 代码案例:Json::Value 构造 JSON

3.5 序列化

3.5.1 核心类介绍

3.5.2 序列化代码示例

3.6 反序列化

3.6.1 核心类介绍

3.6.2 反序列化代码示例

3.6.3 序列化 & 反序列化对比小结

四、Jsoncpp 二次封装

4.1 封装目的

4.2 接口设计思路

4.3 头文件 Myjson.h

4.4 源文件 Myjson.cc 实现

4.5 测试代码 Myjson_test.cc

4.6 Makefile

4.7 编译与运行

4.8 封装优势小结

结束语


前言

在 C++ 后端网络项目开发中,配置文件读取、前后端报文交互、日志结构化输出,都离不开 JSON 这种轻量文本格式。原生 C++ 语言本身并没有内置 JSON 解析能力,我们需要借助第三方库完成 JSON 的构造、序列化与反序列化工作,JsonCpp 便是 C++ 生态里经典、易用的老牌 JSON 开源库。

本篇文章将从 JSON 基础语法入手,先介绍 JsonCpp 的安装方式、核心数据结构Json::Value以及常用 API,手把手演示 JSON 对象构建、序列化输出、字符串反解析等基础操作。 在掌握原生 JsonCpp 使用之后,我们会发现原生接口在工程使用上存在一定冗余,错误处理不够统一。因此我们会基于 JsonCpp 做一层二次封装,自研MyJson工具层,对外提供简洁的序列化、反序列化接口,统一异常与日志处理,屏蔽底层 JsonCpp 的复杂细节。

一、Json 简介

JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,易于人工阅读编写,同时便于机器解析与生成。它源自 JavaScript 的子集,但JSON 本身语言无关,几乎所有编程语言都提供了对应的解析工具。

1.1 基本结构

JSON只有两种核心容器结构,所有复杂数据都由二者组合嵌套而成:

  • 对象:一组无序键值对集合,使用大括号 {}包裹;
  • 数组:一组有序值的集合,使用方括号 []包裹;
  • 值的类型:支持字符串、数字、布尔值 true/false、null,也可以嵌套对象、数组。

1.2 语法规则

JSON有严格的语法规范,书写时必须遵守,否则解析会直接失败:

  • 键值对格式为"key": value,键与值之间用冒号 :分隔;
  • 同一层级多个键值对 / 数组元素之间,使用逗号,分隔;
  • 所有键必须为字符串类型,且必须使用双引号"包裹,单引号不符合 JSON 标准。

1.3 演示示例

{ "name": "John Doe", "age": 30, "is_student": false, "skills": ["JavaScript", "Python", "C++"], "address": { "street": "123 Main St", "city": "Anytown", "state": "CA" } }

1.4 用途

  • 数据存储:作为一种数据格式,用于文件和数据库存储。
  • 数据交换:作为 APIs 和网络请求中数据交换的标准格式。
  • 配置文件:用于应用程序的配置信息。

二、JsonCpp 介绍

JsonCpp是一个高性能的C++ JSON 库,专门用来处理 JSON 数据。
它完整支持 JSON 标准,具备解析 JSON 字符串、生成 JSON 字符串、读写 JSON 对象与数组等能力。

2.1 JsonCpp 安装

Ubuntu 直接命令安装:

sudo apt install libjsoncpp-dev

CentOS 直接命令安装:

CentOS 系统可以通过 yum 包管理器直接安装开发库:

# CentOS7 / CentOS8 sudo yum install jsoncpp-devel

说明:CentOS 的包名后缀是-devel,和 Ubuntu 的-dev命名习惯不一样。

验证是否安装成功:

安装完成后,可通过下面命令查看头文件与库文件,验证是否安装成功:

# 查看头文件 ls /usr/include/jsoncpp/ # 查看库文件 ls /usr/lib/x86_64-linux-gnu/libjsoncpp*

安装完成结果:

三、JsonCpp 使用

3.1 包含头文件

使用该类需要引入头文件:

#include <jsoncpp/json/json.h>

3.2 链接库

编译时,需要链接 jsoncpp 库,编译选项添加:

-ljsoncpp

3.3 Json::Value 类型枚举

Json::ValueType 枚举定义了 JSON 支持的全部数据类型:

namespace Json { enum ValueType { nullValue = 0, ///< 'null' 值 intValue, ///< 有符号整型 uintValue, ///< 无符号整型 realValue, ///< 浮点double值 stringValue, ///< UTF-8字符串 booleanValue, ///< 布尔值 arrayValue, ///< 数组(有序列表) objectValue ///< 对象,键值对集合 };

3.4 中间数据转储类 Json::Value 常用 API

JsonCpp 中最核心的类是 Json::Value,作为内存中间数据转储类。

  • 序列化场景:将 C++ 中的各类数据,先存入Json::Value对象,再转为 JSON 字符串。
  • 反序列化场景:解析 JSON 字符串,解析后的结果全部存放到Json::Value对象,之后从中读取字段。
class JSON_API Value { public: // 拷贝交换 void swap(Value& other); void copy(const Value& other); // 类型转换接口:从Value取出对应类型数据 const char* asCString() const; String asString() const; Int asInt() const; UInt asUInt() const; Int64 asInt64() const; UInt64 asUInt64() const; float asFloat() const; double asDouble() const; bool asBool() const; // 类型判断接口,判断当前Value是什么类型 bool isNull() const; bool isBool() const; bool isInt() const; bool isInt64() const; bool isUInt() const; bool isUInt64() const; bool isIntegral() const; bool isDouble() const; bool isNumeric() const; bool isString() const; bool isArray() const; bool isObject() const; // 数组相关API ArrayIndex size() const; bool empty() const; void clear(); void resize(ArrayIndex newSize); Value& operator[](ArrayIndex index); // 通过下标访问数组元素 Value& append(const Value& value); // 向数组追加元素 // 对象键值访问API Value& operator[](const char* key); Value& operator[](const String& key); void removeMember(const String& key); // 删除指定key bool isMember(const String& key) const; // 判断key是否存在 // 序列化:将Value对象转为格式化JSON字符串 String toStyledString() const; };

核心接口说明:

  1. [] 运算符:用于对象读写键值对。val["name"] = "zhangsan",直接给对象的 key 赋值。
  2. append():数组专用接口,往数组里面追加元素。
  3. asXXX()系列:取值接口。把Json::Value里的数据转成 C++ 基础类型,例如 .asString()、.asDouble()。
  4. isXXX()系列:类型判断。判断当前节点是 null / 对象 / 数组 / 数字等,防止取值类型不匹配。
  5. toStyledString():序列化函数。把内存中的Json::Value转为带缩进格式化的 JSON 字符串。

3.4.1 代码案例:Json::Value 构造 JSON

#include <iostream> #include <jsoncpp/json/json.h> int main() { // 1. 创建Json::Value对象,默认是object类型,用来存储键值对 Json::Value val; val["name"] = "zhangsan"; // []运算符,给对象添加key-value,字符串 val["age"] = 18; // 写入整型 // 给score数组追加浮点元素,val["score"]自动识别为数组 val["score"].append(76.5); val["score"].append(86.5); val["score"].append(96.5); // 2. 手动创建一个数组对象 Json::Value arr(Json::arrayValue); // 创建对象item1,存入id和name,追加到数组arr中 Json::Value item1; item1["id"] = 1; item1["name"] = "Tom"; arr.append(item1); // 创建对象item2,存入id和name,追加到数组arr中 Json::Value item2; item2["id"] = 2; item2["name"] = "Jerry"; arr.append(item2); // 将数组arr作为users字段,存入顶层val对象 val["users"] = arr; // 3. 序列化:Value对象转格式化JSON字符串 std::string json_str = val.toStyledString(); // 打印输出json字符串 std::cout << json_str << std::endl; return 0; }

编译命令参考:

g++ json_test1.cc -o json_test1 -ljsoncpp

运行输出 JSON:

3.5 序列化

序列化的目标:将内存中 Json::Value 对象,转换为 JSON 格式字符串。
JsonCpp 提供StreamWriter作为序列化核心类,采用建造者模式来创建写入器对象,支持自定义序列化格式。

3.5.1 核心类介绍

namespace Json { class JSON_API StreamWriter { public: // 核心写接口:把root的json数据输出到输出流sout virtual int write(Value const& root, OStream* sout); // 建造者工厂基类 class JSON_API Factory { public: virtual StreamWriter* newStreamWriter(); }; }; // 全局辅助函数,根据工厂配置生成json字符串 String JSON_API writeString( StreamWriter::Factory const& factory, Value const& root); // StreamWriter建造者实现类,用来配置序列化参数 class streamWriterBuilder : public StreamWriter::Factory { public: Json::Value settings_; }; }
  • StreamWriter:序列化核心类。write方法完成写入,接收Json::Value对象与输出流,返回值代表执行状态,一般情况下序列化不会失败。
  • StreamWriter::Factory:工厂基类。定义创建StreamWriter的接口。
  • StreamWriterBuilder:建造者派生类。继承工厂,用于配置序列化参数(缩进、换行等),调用 newStreamWriter() 创建 StreamWriter 实例。

设计思路:建造者模式,把对象构造和对象使用分开。我们先通过 Builder 配置输出风格,再生产对应的 StreamWriter,实现灵活自定义 JSON 输出格式。

3.5.2 序列化代码示例

#include <iostream> #include <jsoncpp/json/json.h> #include <memory> #include <sstream> int main() { // 1. 创建Json::Value对象,默认是object类型,用来存储键值对 Json::Value val; val["name"] = "zhangsan"; // []运算符,给对象添加key-value,字符串 val["age"] = 18; // 写入整型 // 给score数组追加浮点元素,val["score"]自动识别为数组 val["score"].append(76.5); val["score"].append(86.5); val["score"].append(96.5); // 2. 创建建造者对象,用于配置序列化输出参数 Json::StreamWriterBuilder builder; builder["indentation"] = ""; // 配置缩进为空字符串,输出紧凑无换行的json // 3. 通过建造者newStreamWriter()生成StreamWriter序列化对象 // 使用unique_ptr自动管理内存,避免内存泄漏 std::unique_ptr<Json::StreamWriter> writer(builder.newStreamWriter()); // 4. 字符串流,用来接收序列化之后的json字符串 std::stringstream ss; // 调用write接口,将val写入字符串流ss int res = writer->write(val, &ss); if (res != 0) { std::cout << "write false" << std::endl; return -1; } // 5. 从stringstream中取出完整json字符串 std::string json_str = ss.str(); std::cout << json_str << std::endl; return 0; }

代码分析:

  1. 数据组装部分:构建Json::Value对象,写入普通键值对与数组,和前面代码基础用法一致。
  2. StreamWriterBuilder builder:实例化建造者,builder["indentation"] = "" 修改缩进配置。
    如果设置" ",输出就是带缩进格式化 JSON;空字符串代表紧凑模式,去掉多余换行空格。
  3. builder.newStreamWriter():工厂方法,动态创建StreamWriter实例,交给 unique_ptr 托管,自动释放内存。
  4. writer->write(val, &ss):序列化核心调用,将 Json::Value 里面的所有数据,写入 C++ 字符串流 stringstream。返回 0 代表成功,非 0 表示异常。
  5. ss.str():从流中提取最终的 JSON 字符串,后续可以打印、写入文件或者网络发送。

编译命令:

g++ json_test2.cc -o json_test2 -ljsoncpp

运行结果展示:

3.6 反序列化

反序列化作用:把 JSON 格式的字符串,解析到内存的 Json::Value 对象中,之后我们就可以读取里面存储的数据。
和序列化一样,JsonCpp 这里同样使用建造者模式,通过CharReader完成解析,CharReaderBuilder用来生产 CharReader 实例。

3.6.1 核心类介绍

namespace Json { class JSON_API CharReader { public: // 核心解析接口,将一段json文本解析存入root virtual bool parse( char const* beginDoc, // json字符串起始地址 char const* endDoc, // json字符串末尾地址 Value* root, // 输出:解析结果存入这个Value对象 String* errs) = 0; // 输出:解析失败时,存放错误信息,默认为nullptr // 工厂基类,定义创建CharReader的接口 class JSON_API Factory { virtual CharReader* newCharReader() const = 0; }; }; // CharReader建造者,继承工厂,可配置解析参数 class JSON_API CharReaderBuilder : public CharReader::Factory { public: Json::Value settings_; CharReader* newCharReader() const override; }; }
  • CharReader:反序列化核心类。parse是解析函数。传入字符串首尾指针,解析成功返回 true;失败返回 false,错误信息存入 errs。
  • CharReader::Factory:工厂抽象基类。规定创建 CharReader 的接口。
  • CharReaderBuilder:建造者实现类,继承 Factory。调用newCharReader()生成 CharReader 对象,也可以通过 settings_配置解析规则。

整体流程:实例化建造者 -> 通过建造者生成 CharReader 对象 -> 调用 parse 完成反序列化。和前面 StreamWriter 序列化的设计完全对应。

3.6.2 反序列化代码示例

#include <iostream> #include <jsoncpp/json/json.h> #include <memory> #include <sstream> int main() { // 错误写法;在字符串中不能直接写双引号,否则会识别出现问题(编译器会识别"{"是字符串,后续代码是乱的) // std::string json_str = "{"age":18,"name":"zhangsan","score":[76.5,86.5,96.5]}"; // 如果需要在字符串中使用双引号,就需要大量转义\",很麻烦 // std::string json_str = "{\"age\":18,\"name\":\"zhangsan\",\"score\":[76.5,86.5,96.5]}"; // C++11原始字符串字面量R"()",不需要手动转义双引号,简化json字符串书写 std::string json_str = R"({"age":18,"name":"zhangsan","score":[76.5,86.5,96.5]})"; // 1.实例化建造者对象 Json::CharReaderBuilder builder; // 2.调用newCharReader生产CharReader,unique_ptr自动管理内存 std::unique_ptr<Json::CharReader> reader(builder.newCharReader()); // 3.定义Value对象,用于存放解析之后的数据 Json::Value val; std::string err_str; // 保存解析失败的错误信息 // 4.核心parse解析接口 // 参数1:字符串起始指针;参数2:字符串末尾指针;参数3:解析输出对象;参数4:错误信息 bool res = reader->parse(json_str.c_str(), json_str.c_str() + json_str.size(), &val, &err_str); if (res == false) { std::cout << "parse json failed, err: " << err_str << std::endl; return -1; } // 5.解析成功,从Json::Value读取数据 std::cout << "name: " << val["name"].asString() << std::endl; std::cout << "age: " << val["age"].asInt() << std::endl; // 读取数组,先判断节点类型,防止数组不存在或者不是数组,避免程序崩溃 int size = val["score"].size(); if (!(val["score"].isNull()) && val["score"].isArray()) { for (int i = 0; i < size; i++) { std::cout << "score: " << val["score"][i].asDouble() << std::endl; } } return 0; }

代码分析:

  • JSON 字符串定义 使用 C++ 原始字符串R"(...)",字符串内部的双引号不用转义。如果不用 R,JSON 里每个"都要写成\",代码可读性很差,也容易写错。
  • parse 解析函数:
    • json_str.c_str():字符串起始地址
    • json_str.c_str() + json_str.size():字符串末尾地址(左闭右开区间)
    • &val:解析完成后,json 数据存入这个 Json::Value 对象
    • &err_str:解析出错,会把错误描述字符串写入 err_str 返回 bool:true 代表解析成功,false 解析失败。
  • 读取基础字段 val["name"].asString():取出 name 节点,转为 std::string;asInt() 取出 age 整型。必须调用 asXX (),不能直接输出 val ["name"]。
  • 数组安全读取先判断 val["score"] 不为空,并且类型是数组,再遍历读取。工程开发中这个判断非常关键:如果 json 缺少 score 字段,直接遍历会触发异常崩溃。

编译命令:

g++ json_test3.cc -o json_test3 -ljsoncpp

运行结果展示:

3.6.3 序列化 & 反序列化对比小结

  • 序列化:StreamWriterBuilder → StreamWriter → write(),内存 Value → JSON 字符串
  • 反序列化:CharReaderBuilder → CharReader → parse(),JSON 字符串 → 内存 Value
    两套接口都是建造者模式,API 设计对称。

四、Jsoncpp 二次封装

4.1 封装目的

原生 jsoncpp 库直接使用时,每次序列化 / 反序列化都需要重复编写建造器对象创建、unique_ptr 管理、错误判断等样板代码。 在项目中反复手写会造成大量重复代码,不利于统一的错误处理与日志打印。 因此我们封装一个 Json 工具类,对外暴露简洁接口,把底层 jsoncpp 的细节全部隐藏。

4.2 接口设计思路

  • 工具类名称:MyJson::JSON,所有接口定义为静态成员函数,不需要实例化对象,直接类名调用
  • 序列化接口:
    • 输入:Json::Value&(内存中的 json 对象)
    • 返回:std::optional<std::string>,成功返回 json 字符串;失败返回std::nullopt
  • 反序列化接口:
    • 输入:std::string&(json 格式字符串)
    • 返回:std::optional<Json::Value>,成功返回解析后的 Value 对象;失败返回std::nullopt

std::optional是 C++17 特性,适合表示可能失败的返回值,可以清晰区分「调用成功带回结果」和「调用失败无结果」,比输出参数、返回错误码更现代。

4.3 头文件 Myjson.h

#ifndef __MYJSON_H__ #define __MYJSON_H__ #include <iostream> #include <jsoncpp/json/json.h> #include <memory> #include <sstream> #include <optional> #include "Mylog.h" using namespace Mylog; namespace MyJson { class JSON { public: // 序列化:Json::Value对象 -> json字符串 static std::optional<std::string> serialize(Json::Value &val); // 反序列化:json字符串 -> Json::Value对象 static std::optional<Json::Value> unserialize(std::string &input); }; } #endif

头文件代码分析:

  1. 引入 optional 头文件,用于接口返回值;同时引入日志模块Mylog.h,失败时打印日志
  2. 放在 MyJson 命名空间,避免和其他库命名冲突
  3. 两个静态函数,对外屏蔽 StreamWriter、CharReaderBuilder 这些底层建造者细节

4.4 源文件 Myjson.cc 实现

#include "Myjson.h" namespace MyJson { std::optional<std::string> JSON::serialize(Json::Value &val) { // 1. 创建序列化建造者,生产StreamWriter Json::StreamWriterBuilder builder; std::unique_ptr<Json::StreamWriter> writer(builder.newStreamWriter()); std::stringstream ss; // 2. 将Value对象写入字符串流 int ret = writer->write(val, &ss); if (ret != 0) { // 序列化失败,使用自定义日志打印错误 ERR("序列化失败!"); return std::nullopt; } // 序列化成功,返回字符串 return ss.str(); } std::optional<Json::Value> JSON::unserialize(std::string &input) { // 1. 创建反序列化建造者,生产CharReader Json::CharReaderBuilder builder; std::unique_ptr<Json::CharReader> reader(builder.newCharReader()); Json::Value val; std::string err_str; // 2. 调用parse进行解析 bool ret = reader->parse(input.c_str(), input.c_str() + input.size(), &val, &err_str); if (ret == false) { // 反序列化失败,打印原始输入字符串与错误信息 ERR("反序列化失败: {}, {}", input, err_str); return std::nullopt; } // 解析成功,返回Value对象 return val; } }

源码逐段分析:

serialize 序列化函数
  • StreamWriterBuilder:建造者,用来创建StreamWriter对象,unique_ptr自动管理内存,不用手动 delete
  • std::stringstream ss:内存流,接收序列化后的文本
  • writer->write(val, &ss):将 Json::Value 中的数据转为 json 文本写入流,返回 0 代表成功
  • 失败:打印日志,返回std::nullopt;成功取出 stringstream 里的字符串返回
unserialize 反序列化函数
  • CharReaderBuilder:反序列化建造者,创建 CharReader 解析器
  • reader->parse():核心解析函数,传入字符串首尾指针,解析结果存入 val,错误信息存入 err_str
  • 解析失败:日志打印原始字符串和错误原因,返回std::nullopt;成功返回解析好的Json::Value

4.5 测试代码 Myjson_test.cc

#include "Myjson.h" using namespace MyJson; void Serialize_test() { Json::Value val; val["name"] = "zhangsan"; val["age"] = 18; val["score"].append(76.5); val["score"].append(86.5); val["score"].append(96.5); // 错误写法: // std::string json_str = serialize(val); // std::cout << json_str << std::endl; auto result = JSON::serialize(val); std::string json_str = result.value(); std::cout << json_str << std::endl; } void UnSerialize_test() { std::string json_str = R"({"age":18,"name":"zhangsan","score":[76.5,86.5,96.5]})"; auto result = JSON::unserialize(json_str); Json::Value val = result.value(); std::cout << "name: " << val["name"].asString() << std::endl; std::cout << "age: " << val["age"].asInt() << std::endl; int size = val["score"].size(); if (!(val["score"].isNull()) && val["score"].isArray()) { for (int i = 0; i < size; i++) { std::cout << "score: " << val["score"][i].asDouble() << std::endl; } } } int main() { Serialize_test(); UnSerialize_test(); return 0; }

测试代码说明:

  • Serialize_test:构造 Json::Value 对象,调用封装好的 JSON::serialize 一键序列化,拿到 json 字符串
  • UnSerialize_test:传入 json 字符串,调用 JSON::unserialize 一键反序列化,得到 Json::Value,读取内部字段
  • 使用方式极大简化,不再需要每次手写建造者、parse、write 等底层代码,业务代码只关心数据本身

4.6 Makefile

CFLAGS=-I../source myjson_test:Myjson_test.cc ../source/Myjson.cc ../source/Mylog.cc g++ -o $@ $^ -std=c++17 $(CFLAGS) -lpthread -ljsoncpp -lfmt -lspdlog .PHONY:clean clean: rm -rf myjson_test
  • -std=c++17:必须开启 C++17,std::optional是 C++17 引入的特性
  • -ljsoncpp:链接 jsoncpp 库;同时链接日志相关 spdlog、fmt 库
  • -I../source:指定头文件搜索路径,找到 Myjson.h、Mylog.h

4.7 编译与运行

4.8 封装优势小结

  • 简化业务代码:业务层一行调用,不用重复写建造器、内存管理、错误处理
  • 统一错误处理:所有 json 解析失败,统一走日志输出,方便项目问题排查
  • 底层隔离:后续更换 json 库(例如 nlohmann),只需要修改 Myjson.cc 内部实现,业务代码完全不用改动
  • 内存安全:内部使用 unique_ptr,自动释放解析器 / 写入器,避免内存泄漏

注意点:调用.value()获取 optional 里面的值,必须保证 optional 有值;工程中严谨写法要先判断 if(result.has_value()),再调用 value (),否则会抛异常。

结束语

到这里,本篇 JsonCpp 的实战内容就全部结束了。我们从 JSON 基础概念出发,学习了 JsonCpp 库的安装、核心 API 的调用,亲手完成 JSON 对象构造、序列化与反序列化等基础功能。

原生 JsonCpp 功能完备,但直接在业务代码中裸用会带来代码冗余、错误处理零散等问题。因此我们进行了二次封装,搭建MyJson工具层,将底层库的复杂接口做收拢,对外暴露简洁易用的序列化、反序列化能力,统一错误返回与日志处理。同时也探讨了工具类接口的设计思路,理解为什么序列化函数适合设计为静态成员函数,体会第三方库封装的工程思想。

本次封装的 MyJson 模块可以直接嵌入到 C++ 后端项目中,用于配置解析、网络报文处理。这个二次封装的思路也具备通用性,后续遇到其他第三方库时,都可以借鉴这套思路,屏蔽底层细节、统一异常处理,降低业务层的开发成本。

返回列表