nlohmann json解析特殊数据
1.引言
目前一些项目中可能会使用nlohmann json来解析json字符。比如在一些请求的处理中,发送请求的封装、返回数据的解析都是通过调用现有接口完成的。
但针对部分结构比较特殊的数据,常规的解析方法无法完成解析,需要一些特殊手段。
本文即针对此类情况,阐述目前可行的几种方法。
2.常规json解析
目前中主要使用json方法如下:
- 序列化成string:dump()
- string反序列化成json:parse()
- 自定义值序列化成json:to_json(json, T& value)
- json反序列化成结构体:from_json(json, T& value);
在解析返回的数据时,一般先采用parse将数据从string反序列化为json,再调用from_json将json数据反序列化成结构体。
但此种方法也有局限性。
因为调用from_json即需要生成一组序列化和反序列化侵入式接口,即Json串和结构体各成员的对应关系
这也就需要在请求解析前知道要处理的架构是什么。比如
Response: { "error_code": 0, "A": {"config": {"enabled": "1", "id": "1" } } }
要想解析这个请求,那么就要定义对应的接口
struct Config {
std::string enabled;
std::string id;
NLOHMANN_DEFINE_TYPE_INTRUSIVE_NO_EXCEPTION(Config, enabled, id);
};
struct ConfigResponse {
Config config;
int error_code;
NLOHMANN_DEFINE_TYPE_INTRUSIVE_NO_EXCEPTION(ConfigResponse, error_code, config);
};
或者在反序列化成Json后使用get接口手动解析
const auto root = json:parse(response.data);
std::string enabled = root["enable"].get<std::string>;
3.局限性
上述两种方法都适用于在解析前已经知道了返回的结构以及各个节点的名称,并能够定义出对应的结构体、序列化和反序列化侵入式接口才行。
考虑下面的情况:
Response: { "error_code": 0, "plugin": [ {"002": {"id":"002", "name":"AI"}}, { "001": { "id":"001", "name":"DNS" } } ] }
请求返回的数组两个成员的key不同,如果定义结构体进行解析,那么数组成员的下一级必须是一样的否则定义不出数组。
比如:
struct Plugins
{
std::vector<Plugin> plugin;
};
struct Plugin{ SinglePlugin A;};
struct SinglePlugin {std::string id; std::string name;};
那么A的命名是"LW001"还是"LW002"呢?无论定义哪一种,都无法完整解析。
4.解决方法
4.1替换
但通过分析可以发现,导致无法解析的根本原因是数组各项的key命名不一致。
那么很容易想到,可否替换成一致的?而且key也是无用信息,在id中已经包含了这个key,那就可以放心的替换了。
将key统一替换
比如都替换成LW,那么我的Plugin结构体内的SinglePlugin对象A就可以命名为LW;此时即可调用现有接口解析,但只适用于有规律、统一的形式,可行性不高。
4.2手动解析
当然也可以采用最普通的手动解析。
如果命名有规则,比如知道了每个命名都是0->n,那么可以遍历获取:
当然这种方法局限性比较强,因为各项的key有时可能并无规律,且使用get访问无value的key会抛出异常。
既然key是无用值,可不可以直接跳过呢?
4.3跳过key
nlohmann json提供了遍历所有items的接口:
//@brief helper to access iterator member functions in range-based for
iteration_proxy<iterator> items() noexcept { return iteration_proxy<iterator>(*this); }
以及获取一个object的value的接口
/* @brief return the value of an iterator @pre The iterator is initialized; i.e. `m_object != nullptr`. */
reference value() const { return operator*();}
那自然就可以通过获取item的value,将key跳过
Response: { "error_code": 0, "plugin": [ {"002": {"id":"002", "name":"AI"}}, { "001": { "id":"001", "name":"DNS" } } ] }
代码如下:
auto pjRoot = json::parse(data);
auto &plugins = pjRoot["plugin"];
for (const auto &item : plugins.items())
{
auto itemValue = item.value().dump();
for (const auto &singleItem : item.value().items())
{
auto singleItemValue = singleItem.value().dump();
PluginInfo info;
json::fromJson(singleItem.value(), info);
infos.push_back(info);
}
}
5.其他应用——结构体复用
对于一些解析结构体类似的地方,定义两个结构体有点浪费,可以解析器前对数据特殊处理
比如:
获取信息返回的kind为int类型,且所有的信息组成是数组。
添加信息返回的结果中kind为string类型,且返回的不是数组
那么可以以获取信息为基准设计结构体
在解析添加信息返回的数据时,手动处理一下:
/* 手动处理kind */
auto jsonExp = jsonMap["result"]["data"];
std::string kind = jsonExp["kind"];
jsonExp["kind"] = atoi(kind.c_str()); /* 手动处理invoice */
jsonMap["result"]["data"] = std::vector<Exp>();
jsonMap["result"]["data"].emplace_back(jsonExp);
后面正常解析即可。
6.总结
本文主要针对工作中遇到的特殊类型的解析方法进行阐述。对于多余的key,可以通过替换成统一的名称、手动解析(已知key值遍历、不知key值跳过)的方法解析。需要注意,直接使用get接口存在一定安全问题,建议封装函数捕获异常并处理。