JSON數(shù)據(jù)自動(dòng)生成C++結(jié)構(gòu)體
生成的c++結(jié)構(gòu)體基于nlohmann/json進(jìn)行解析,實(shí)現(xiàn)了類似JavaBean和C#中
JsonConvert.SerializeObject的效果,將c++結(jié)構(gòu)體與Json數(shù)據(jù)結(jié)構(gòu)進(jìn)行了映射,使得json解析成c++對(duì)象這一過程對(duì)上層屏蔽,可以實(shí)現(xiàn)快速開發(fā)。
背景
在編寫服務(wù)端程序時(shí),除了和系統(tǒng)交互、業(yè)務(wù)邏輯的內(nèi)部實(shí)現(xiàn),最主要的一部分就是和客戶端打交道?,F(xiàn)在web服務(wù)器開發(fā),最流行的數(shù)據(jù)傳輸格式基本是Json、Xml、Protobuf,其中Json格式由于其和javascript語言對(duì)象模型的兼容性最好,成為b/s模型下最常用的數(shù)據(jù)傳輸格式。
在高級(jí)語言如Java、C#中,有一些內(nèi)置的庫實(shí)現(xiàn)了語言對(duì)象模型和Json數(shù)據(jù)間的自動(dòng)轉(zhuǎn)換,這一點(diǎn)著實(shí)讓cpper羨慕不已。雖然c++也有一些成熟的開源解析庫如nlohmann/json、RapidJson、Jsoncpp等,讓解析Json已經(jīng)變得相對(duì)簡(jiǎn)單高效,但讓程序員手動(dòng)根據(jù)字段進(jìn)行逐一解析仍然是一件比較浪費(fèi)時(shí)間的事情。在性能要求沒那么高的場(chǎng)景下(絕大多數(shù)情況),如果能實(shí)現(xiàn)c++對(duì)象和Json數(shù)據(jù)的自動(dòng)轉(zhuǎn)換,無疑能大幅提高開發(fā)效率,并減少因程序員手誤導(dǎo)致的解析錯(cuò)誤。
因此,考慮基于nlohmann/json解析庫,實(shí)現(xiàn)c++和Json數(shù)據(jù)的對(duì)象映射自動(dòng)代碼生成。
nlohmann/json基礎(chǔ)
nlohmann/json是基于c++11特性實(shí)現(xiàn)的一個(gè)開源Json解析庫,其在github上的start數(shù)達(dá)到了13.4k,開源協(xié)議為MIT license, 因此可以作為商用項(xiàng)目使用。整個(gè)解析庫只有一個(gè)json.hpp,可以非常方便的移植到項(xiàng)目程序中。且解析庫提供的接口非常人性化,上手容易,學(xué)習(xí)成本較低。
而對(duì)象映射這一功能,nolhmann庫其實(shí)已經(jīng)替我們實(shí)現(xiàn)了,舉官方的一個(gè)例子說明:
namespace ns {
// a simple struct to model a person
struct person {
std::string name;
std::string address;
int age;
};
}
// create a person
ns::person p {"Ned Flanders", "744 Evergreen Terrace", 60};
// conversion: person -> json
json j = p;
std::cout << j << std::endl;
// {"address":"744 Evergreen Terrace","age":60,"name":"Ned Flanders"}
// conversion: json -> person
auto p2 = j.get<ns::person>();
// that's it
assert(p == p2);
};
可以看到,程序當(dāng)中,我們只需要定義好一個(gè)Person結(jié)構(gòu)體,再定義一個(gè)json對(duì)象,兩者即可用=進(jìn)行隱式轉(zhuǎn)換。當(dāng)然,實(shí)現(xiàn)隱式轉(zhuǎn)換的前提是定義相應(yīng)的to_json和from_json函數(shù),該例中:
using nlohmann::json;
namespace ns {
void to_json(json& j, const person& p) {
j = json{{"name", p.name}, {"address", p.address}, {"age", p.age}};
}
void from_json(const json& j, person& p) {
j.at("name").get_to(p.name);
j.at("address").get_to(p.address);
j.at("age").get_to(p.age);
}
} // namespace ns
如上,我們得出基于nlohmann/json實(shí)現(xiàn)對(duì)象映射的核心步驟:
- 定義一個(gè)c++結(jié)構(gòu)體
- 編寫該c++結(jié)構(gòu)體轉(zhuǎn)換為json對(duì)象的
to_json函數(shù) - 編寫json對(duì)象轉(zhuǎn)換為該c++結(jié)構(gòu)體的
from_json函數(shù)
Python自動(dòng)生成C++代碼
如上介紹,對(duì)于一個(gè)現(xiàn)有的JSON數(shù)據(jù),我們還是需要編寫上述機(jī)械化的代碼,這些完全可以找出格式上的規(guī)則使用Python進(jìn)行自動(dòng)化代碼生成。在github上搜索了相關(guān)項(xiàng)目后,最終參考了一個(gè)項(xiàng)目的實(shí)現(xiàn)思路,使生成的代碼采用nlohmman/json進(jìn)行解析。
注: 原項(xiàng)目生成的代碼使用的JSON庫是
<cppRest/json.h>,項(xiàng)目鏈接kcris/json2cpp。
具體的生成代碼不做詳述,后面會(huì)將源碼Po上,有興趣的可以看一下。基本思想就是根據(jù)Json字段名進(jìn)行類型區(qū)分,對(duì)于對(duì)象類型進(jìn)行遞歸生成。最終的生成結(jié)果采用一個(gè)Object對(duì)象類型對(duì)應(yīng)一個(gè).h頭文件和.cpp文件的形式。
C++解析、組裝函數(shù)封裝(可選)
nlohmann庫的隱式解析會(huì)拋出異常,我們需要捕獲異常并進(jìn)行相應(yīng)處理。因此,在cpp中考慮對(duì)這部分進(jìn)行了二次封裝,使外層調(diào)用者不需要關(guān)心異常處理。此外,通信層傳輸?shù)腏SON格式有些是不帶外層節(jié)點(diǎn)的,有些是帶外層節(jié)點(diǎn)的,我們也需要對(duì)這兩種格式做適配。
這部分有需要可以自己寫一下,沒有太多工作量。
快速開始
生成cpp文件
為了方便使用,基于tkinter做了一個(gè)界面,打包成了一個(gè)EXE工具。目前該工具只支持包含外層節(jié)點(diǎn)的JSON數(shù)據(jù)格式。
- 打開Json2cppTool.exe
- 填入JSON數(shù)據(jù)或者選擇JSON數(shù)據(jù)文件
- 選擇輸出路徑
- 點(diǎn)擊生成
JSON數(shù)據(jù)如下:
{
"UserInfoDetail": {
"mode": "",
"EmployeeNoList": [
{
"employeeNo": ""
}
]
}
}

文件導(dǎo)入工程
生成文件如下:
UserInfoDetail.h
UserInfoDetail.cpp
EmployeeNoList.h
EmployeeNoList.cpp
C++程序中使用
序列化
UserInfoDetail user_info_detail ;
user_info_detail.m_mode = "all";
string str_json = JsonSerialize(user_info_detail);
反序列化
ResponseStatus response_status;
if (!JsonDeserialize(str_raw, response_status))
{
return false;
}
That's it!
對(duì)于列表
std::list<T>類型的節(jié)點(diǎn),我們也無需做特殊處理,nlohmann已經(jīng)將列表和JSON Array間的轉(zhuǎn)換實(shí)現(xiàn)掉了。
進(jìn)階用法
序列化時(shí)控制是否輸出外層節(jié)點(diǎn)
默認(rèn)會(huì)輸出外層節(jié)點(diǎn),但是可以通過JsonSerialize(Obj,false)來指定不生成外層節(jié)點(diǎn)。
示例:
string str_json = JsonSerialize(user_info_detail, false);
輸出的JSON:
{
"mode": "",
"EmployeeNoList": [
{
"employeeNo": ""
}
]
}
指定組裝的節(jié)點(diǎn)
在默認(rèn)情況下,自動(dòng)映射會(huì)將c++結(jié)構(gòu)體中的所有成員均映射到JSON中的節(jié)點(diǎn)。
但有的場(chǎng)景,我們希望發(fā)送給客戶端或服務(wù)端的JSON數(shù)據(jù)中,只包含部分必填字段。
自動(dòng)生成的c++結(jié)構(gòu)體中包含了一個(gè)std::set<std::string> m_visibleSet;成員,通過該成員控制需要輸出的節(jié)點(diǎn)。
示例:
UserInfoDetail user_info_detail ;
user_info_detail.m_mode = "all";
user_info_detail.m_visibleSet = {
"mode",
};
string str_json = JsonSerialize(clearCfg);
輸出的JSON:
{
"UserInfoDetail": {
"mode": "all"
}
}
指定需要忽略的節(jié)點(diǎn)
在默認(rèn)情況下,自動(dòng)映射會(huì)將c++結(jié)構(gòu)體中的所有成員均映射到JSON中的節(jié)點(diǎn)。
但有的場(chǎng)景,我們希望發(fā)送給客戶端或服務(wù)端的JSON數(shù)據(jù)中,能忽略某些節(jié)點(diǎn)。
自動(dòng)生成的c++結(jié)構(gòu)體中包含了一個(gè)std::set<std::string> m_hiddenSet;成員,通過該成員控制需要忽略的節(jié)點(diǎn)。
示例:
UserInfoDetail user_info_detail ;
user_info_detail.m_mode = "all";
user_info_detail.m_hiddenSet = {
"EmployeeNoList",
};
string str_json = JsonSerialize(clearCfg);
輸出的JSON:
{
"UserInfoDetail": {
"mode": "all"
}
}
附錄
源碼見json2cppTool