Json文件解析(下)
Json文件解析(下)
代码地址:https://github.com/nlohmann/json
从STL容器转换
任何序列容器(std::array,std::vector,std::deque,std::forward_list,std::list),其值可以被用于构建JSON值(例如,整数,浮点数,布尔值,字符串类型,或者再次在本节中描述STL容器)可被用于创建JSON阵列。这同样适用于类似的关联容器(std::set,std::multiset,std::unordered_set,std::unordered_multiset),但是在这些情况下,数组中的元素的顺序取决于元素是如何在各个STL容器排序。
std ::矢量< INT > c_vector { 1,2,3,4 };
json j_vec(c_vector);
// [1、2、3、4]
std ::双端队列< 双 > c_deque { 1.2,2.3,3.4,5.6 };
json j_deque(c_deque);
// [1.2、2.3、3.4、5.6]
std :: list < bool > c_list { true,true,false,true };
json j_list(c_list);
// [true,true,false,true]
std ::修饰符Modifiers < 的int64_t > c_flist { 12345678909876,23456789098765,34567890987654,45678909876543 };
json j_flist(c_flist);
// [12345678909876、23456789098765、34567890987654、45678909876543]
std ::阵列< 无符号 长,4 > c_array {{ 1,2,3,4 }};
json j_array(c_array);
// [1、2、3、4]
std :: set <std :: string> c_set { “一个”,“两个”,“三个”,“四个”,“一个” };
json j_set(c_set); //仅使用“一个”的一个条目
// // [“四个”,“一个”,“三个”,“两个”]
std :: unordered_set <std :: string> c_uset { “一”,“二”,“三”,“四”,“一” };
json j_uset(c_uset); //仅使用“一个”的一个条目
// //可能是[“两个”,“三个”,“四个”,“一个”]
std :: multiset <std :: string> c_mset { “一个”,“两个”,“一个”,“四个” };
json j_mset(c_mset); //使用两个用于“一个”的条目
//也许[[一个”,“两个”,“一个”,“四个”]
std :: unordered_multiset <std :: string> c_umset { “一”,“二”,“一”,“四” };
json j_umset(c_umset); //使用两个用于“一个”的条目
//也许[[一个”,“两个”,“一个”,“四个”]
同样,任何缔键值容器(std::map,std::multimap,std::unordered_map,std::unordered_multimap),其键可以构造一个std::string,并且其值可以被用于构建JSON值(见上文实施例)可用于创建一个JSON对象。请注意,在使用多图的情况下,JSON对象中仅使用一个键,并且该值取决于STL容器的内部顺序。
std :: map <std :: string,int > c_map {{ “一个”,1 },{ “两个”,2 },{ “三个”,3 }};
json j_map(c_map);
// {“一个”:1,“三个”:3,“两个”:2}
std :: unordered_map < const char *,double > c_umap {{ “一个”,1.2 },{ “两个”,2.3 },{ “三个”,3.4 }};
json j_umap(c_umap);
// {“一个”:1.2,“两个”:2.3,“三个”:3.4}
std :: multimap <std :: string,bool > c_mmap {{ “一个”,true },{ “两个”,true },{ “三个”,false },{ “三个”,true }};
json j_mmap(c_mmap); //仅使用键“三”的一个条目
// //也许{“一个”:true,“两个”:true,“三个”:true}
std :: unordered_multimap <std :: string,bool > c_ummap {{ “一”,true },{ “二”,true },{ “三”,false },{ “三”,true }};
json j_ummap(c_ummap); //仅使用键“三”的一个条目
// //也许{“一个”:true,“两个”:true,“三个”:true}
JSON指针和JSON补丁
该库支持JSON指针(RFC 6901)作为解决结构化值的替代方法。除此之外,JSON Patch(RFC 6902)允许描述两个JSON值之间的差异-有效地允许Unix已知的patch和diff操作。
// JSON值
json j_original = R“( {{
” baz“:[” one“,” two“,” three“],
” foo“:” bar“
} )” _json;
//使用JSON指针(RFC 6901)访问成员
j_original [ “ / baz / 1 ” _json_pointer];
// “两个”
//一个JSON修补程序(RFC 6902)
json j_patch = R“( [[
{” op“:” replace“,” path“:” / baz“,” value“:” boo“},
{” op“:” add “,” path“:” / hello“,” value“:[” world“]},
{” op“:” remove“,” path“:” / foo“}
] )” _json;
//应用补丁
json j_result = j_original.patch(j_patch);
// {
// “ baz”:“ boo”,
// “ hello”:[“ world”]
// }
//根据两个JSON值计算JSON补丁
json :: diff(j_result,j_original);
// [
// {“ op”:“ replace”,“ path”:“ / baz”,“ value”:[“ one”,“ Two”,“ three”]},
// {“ op”:“ remove“,” path“:” / hello“},
// {” op“:” add“,” path“:” / foo“,” value“:” bar“}
// ]
JSON合并补丁
该库支持JSON合并补丁(RFC 7386)作为补丁格式。没有使用JSON指针(请参见上文)来指定要操作的值,而是使用一种语法来描述更改,该语法与拟修改的文档非常相似。
// JSON值
json j_document = R“( {
” a“:” b“,
” c“:{
” d“:” e“,
” f“:” g“
}
}} )” _json;
//补丁
json j_patch = R“( {
” a“:” z“,
” c“:{
” f“:null
}
} )” _json;
//应用补丁
j_document.merge_patch(j_patch);
// {
// “ a”:“ z”,
// “ c”:{
// “ d”:“ e”
// }
// }
隐式转换
支持的类型可以隐式转换为JSON值。
建议不要使用隐式转换从一个JSON值。可以在此处找到有关此建议的更多详细信息。
//字符串
std :: string s1 = “世界好!” ;
json js = s1;
自动 s2 = js.get <std :: string>();
//不推荐
std :: string s3 = js;
std :: string s4;
s4 = js;
//布尔
值bool b1 = true ;
json jb = b1;
自动 b2 = jb.get < bool >();
//不推荐
bool b3 = jb;
布尔 b4;
b4 = jb;
//数字
int i = 42 ;
json jn = i;
自动 f = jn.get < double >();
//不推荐使用
double f2 = jb;
双 f3;
f3 = jb;
//等
请注意,char类型不会自动转换为JSON字符串,而是自动转换为整数。必须明确指定转换为字符串:
char ch = ' A ' ; // ASCII值65
json j_default = ch; //存储整数65
json j_string = std :: string(1,ch); //存储字符串“ A”
任意类型转换
每种类型都可以用JSON序列化,而不仅仅是STL容器和标量类型。通常,会按照以下方式进行操作:
名称空间 ns {
// //一个简单的结构,用于对人员进行建模
struct person {
std :: string名称;
std :: string地址;
INT年龄;
};
}
ns :: person p = { “ Ned Flanders ”,“ 744 Evergreen Terrace ”,60 };
//转换为JSON:将每个值复制到JSON对象中
json j;
j [ “ name ” ] = p.name;
j [ “ address ” ] = p.address;
j [ “ age ” ] = p.age;
// ...
//从JSON转换:复制JSON对象中的每个值
ns :: person p {
j [ “ name ” ]。得到 <std :: string>(),
j [ “ address ” ]。得到 <std :: string>(),
j [ “年龄” ]。获取 < int >()
};
可以工作,但是有很多样板...幸运的是,有更好的方法:
//创建一个人
ns :: person p { “ Ned Flanders ”, “ 744 Evergreen Terrace ”, 60 };
//转换:person-> json
json j = p;
std :: cout << j << std :: endl;
// {“地址”:“ 744 Evergreen Terrace”,“年龄”:60,“名称”:“ Ned Flanders”}
//转换:
json- > person auto p2 = j.get <ns :: person>();
//就是
断言(p == p2);
基本用法
要使与一种类型一起使用,只需提供两个功能:
使用 nlohmann :: json;
命名空间 ns {
void to_json(json&j,const person&p){
j = json {{ “ name ”,p。名称 },{ “地址”,第 地址 },{ “年龄”,第 年龄 }};
}
无效 from_json(const json&j,person&p){
j。在(“名称”)。get_to(第名);
j。在(“地址”)。get_to(第地址);
j。在(“年龄”)。get_to(第年龄);
}
} //名称空间ns
就这样!当json使用类型调用构造函数时,自定义to_json方法将被自动调用。同样,在调用get<your_type>()或时get_to(your_type&),from_json将调用该方法。
一些重要的事情:
- 这些方法必须位于类型的名称空间(可以是全局名称空间)中,否则库将无法找到(在本示例中,位于定义的名称空间ns中person)。
- 在使用这些转换的任何地方,这些方法都必须可用(例如,必须包含适当的标头)。请查看问题1108,了解否则可能发生的错误。
- 使用时get<your_type>(),your_type 必须为DefaultConstructible。(有一种绕过此要求的方法,将在后面介绍。)
- 在函数中from_json,使用函数at()访问对象值而不是operator[]。万一键不存在,at将引发可以处理的异常,而operator[]表现出未定义的行为。
- 无需为STL类型添加序列化器或反序列化器,例如std::vector:该库已经实现了这些。
使用宏简化生活
如果只想对一些结构进行序列化/反序列化,则to_json/ from_json函数可能会很多。
只要(1)想要将JSON对象用作序列化和(2)要将成员变量名称用作该对象中的对象键,就有两个宏可以使生活更轻松:
- NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(name, member1, member2, ...) 将在要为其创建代码的类/结构的名称空间内定义。
- NLOHMANN_DEFINE_TYPE_INTRUSIVE(name, member1, member2, ...)在要为其创建代码的类/结构中定义。该宏还可以访问私有成员。
在两个宏中,第一个参数是类/结构的名称,其余所有参数都为成员命名。
例子
上面结构的to_json/ from_json函数person可以通过以下方式创建:
命名空间 ns {
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(人,姓名,地址,年龄)
}
这是一个NLOHMANN_DEFINE_TYPE_INTRUSIVE需要私人成员的示例:
命名空间 ns {
类 地址 {
私有:
std :: string street;
INT housenumber;
INT邮编;
公开:
NLOHMANN_DEFINE_TYPE_INTRUSIVE(地址,街道,门牌号,邮政编码)
};
}
如何转换第三方类型?
这需要更高级的技术。但首先,让看看这种转换机制是如何工作的:
该库使用JSON序列化器将类型转换为json。默认的序列化器nlohmann::json为nlohmann::adl_serializer(ADL表示参数依赖查找)。
这样实现(简化):
template < typename T>
struct adl_serializer {
static void to_json(json&j,const T&value){
//在T的命名空间中调用“ to_json”方法
}
static void from_json(const json&j,T&value){
//相同,但使用“ from_json”方法
}
};
当可以控制类型的名称空间时,此序列化程序可以正常工作。但是,关于boost::optional或std::filesystem::path(C ++ 17)呢?劫持boost名称空间是非常糟糕的,并且向模板添加模板专业化以外的内容是非法的std。
为了解决这个问题,需要adl_serializer在nlohmann名称空间中添加的特殊化,这是一个示例:
//部分专业化(也可以完全专业化)
名称空间 nlohmann {
模板 <类型名 T>
struct adl_serializer <boost :: optional <T >> {
静态 void to_json(json&j, const boost :: optional <T>&opt){
如果(opt == boost :: none){
j = nullptr ;
} 其 {
j = * opt; //这将调用adl_serializer <T> :: to_json将
//发现T的命名空间中的自由函数to_json!
}
}
静态 无效 from_json(const json&j,boost :: optional <T>&opt){
if(j。is_null()){
opt = boost :: none;
} 其 {
选择= j。得到 <T>(); //与上述相同,但
//使用 adl_serializer <T> :: from_json
}
}
};
}
如何get()用于非默认的可构造/不可复制类型?
如果类型为MoveConstructible,则有一种方法。还需要专门化adl_serializer,但要有一个特殊的from_json重载:
struct move_only_type {
move_only_type()= 删除 ;
move_only_type(int ii):i(ii){}
move_only_type(const move_only_type&)= delete ;
move_only_type(move_only_type &&)= 默认值;
诠释 I;
};
名称空间 nlohmann {
template <>
struct adl_serializer <move_only_type> {
//注意:返回类型不再是'void',该方法仅
//使用一个参数
static move_only_type from_json(const json&j){
return {j。get < int >()};
}
//这就是陷阱!必须提供to_json方法!否则
//
将//无法将move_only_type转换为json,因为完全//对该类型的adl_serializer进行了专门化//
static void to_json(json&j,move_only_type t){
j = t ;
}
};
}
可以编写自己的序列化器吗?(高级使用)
是。可能需要看一下unit-udt.cpp测试套件,以查看一些示例。
如果编写自己的序列化器,则需要做一些事情:
- 使用不同的basic_json别名nlohmann::json(的最后一个模板参数basic_json是JSONSerializer)
- basic_json在所有to_json/ from_json方法中使用别名(或模板参数)
- 使用nlohmann::to_json以及nlohmann::from_json何时需要ADL
这是一个示例,没有简化,仅接受大小<= 32的类型,并使用ADL。
// //应该使用void作为第二个模板参数
// //如果不需要对T
template < typename T, typename SFINAE = typename std :: enable_if < sizeof(T)<= 32 > :: type进行编译时检查 >
struct less_than_32_serializer {
模板 <类型名称BasicJsonType>
静态 void to_json(BasicJsonType&j,T值){
//要使用ADL,并
使用 nlohmann :: to_json调用正确的to_json重载;//这个方法由adl_serializer调用,
//这就是
to_json(j,value)发生魔术的地方;
}
template < typename BasicJsonType>
静态 无效 from_json(const BasicJsonType&j,T&value){
//
使用 nlohmann :: from_json进行相同操作;
from_json(j,值);
}
};
重新实现序列化器时要非常小心,如果不注意,可能会导致堆栈溢出:
模板 < 类型名 T,无效 >
结构 bad_serializer
{
template < typename BasicJsonType>
static void to_json(BasicJsonType&j,const T&value){
//这将调用BasicJsonType :: json_serializer <T> :: to_json(j,value);
//如果BasicJsonType :: json_serializer == bad_serializer ...糟糕!
j =值;
}
template < typename BasicJsonType>
static void to_json(const BasicJsonType&j,T&value){
//这将调用BasicJsonType :: json_serializer <T> :: from_json(j,value);
//如果BasicJsonType :: json_serializer == bad_serializer ...糟糕!
值= j。模板 获取 <T>(); //哎呀!
}
};
专门进行枚举转换
默认情况下,枚举值以整数形式序列化为JSON。在某些情况下,这可能会导致不良行为。如果在将数据序列化为JSON之后对枚举进行了修改或重新排序,则较晚的反序列化JSON数据可能是未定义的或与原始预期值不同的枚举值。
可以更精确地指定给定枚举如何映射到JSON和从JSON映射,如下所示:
//示例枚举类型声明
枚举 TaskState {
TS_STOPPED,
TS_RUNNING,
TS_COMPLETED,
TS_INVALID = -1,
};
//将TaskState值作为字符串
NLOHMANN_JSON_SERIALIZE_ENUM 映射到JSON(TaskState,{
{TS_INVALID,nullptr },
{TS_STOPPED,“已停止” },
{TS_RUNNING,“正在运行” },
{TS_COMPLETED,“已完成”),
})
的NLOHMANN_JSON_SERIALIZE_ENUM()宏声明一组to_json()/ from_json()功能型TaskState,同时避免重复和样板序列化代码。
用法:
//以字符串形式枚举JSON
json j = TS_STOPPED;
断言(j == “ stopped ”);
//枚举的json字符串
json j3 = “正在运行”;
断言(j3.get <TaskState>()== TS_RUNNING);
//要枚举的未定义json值(其中,上面的第一个地图项是默认值)
json jPi = 3.14 ;
断言(jPi.get <TaskState>()== TS_INVALID);
就像上面的任意类型转换一样,
- NLOHMANN_JSON_SERIALIZE_ENUM() 必须在枚举类型的名称空间(可以是全局名称空间)中声明,否则库将无法找到,并且将默认为整数序列化。
- 使用转换的任何地方都必须可用(例如,必须包含适当的标题)。
其要点:
- 使用时get<ENUM_TYPE>(),未定义的JSON值将默认为地图中指定的第一对。仔细选择该默认对。
- 如果在地图中多次指定枚举或JSON值,则在与JSON进行相互转换时,将从地图顶部的第一个匹配项返回。
二进制格式(BSON,CBOR,MessagePack和UBJSON)
尽管JSON是一种无处不在的数据格式,但不是一种非常紧凑的格式,适合通过网络进行数据交换。因此,该库支持 BSON(二进制JSON),CBOR(简明二进制对象表示形式),MessagePack和UBJSON(通用二进制JSON规范),以将JSON值有效地编码为字节向量并对该向量进行解码。
//创建一个JSON值
json j = R“( {” compact“:true,” schema“:0} )” _json;
//序列化为BSON
std :: vector <std :: uint8_t > v_bson = json :: to_bson(j);
// 0x1B,0x00、0x00、0x00、0x08、0x63、0x6F,0x6D,0x70、0x61、0x63、0x74、0x00、0x01、0x10、0x73、0x63、0x68、0x65、0x6D,0x61、0x00、0x00 0x00、0x00、0x00
//往返
json j_from_bson = json :: from_bson(v_bson);
//序列化为CBOR
std :: vector <std :: uint8_t > v_cbor = json :: to_cbor(j);
// 0xA2、0x67、0x63、0x6F,0x6D,0x70、0x61、0x63、0x74、0xF5、0x66、0x73、0x63、0x68、0x65、0x6D,0x61、0x00
//往返
json j_from_cbor = json :: from_cbor(v_cbor);
//序列化为MessagePack
std :: vector <std :: uint8_t > v_msgpack = json :: to_msgpack(j);
// 0x82、0xA7、0x63、0x6F,0x6D,0x70、0x61、0x63、0x74、0xC3、0xA6、0x73、0x63、0x68、0x65、0x6D,0x61、0x00
//往返
json j_from_msgpack = json :: from_msgpack(v_msgpack);
//序列化为UBJSON
std :: vector <std :: uint8_t > v_ubjson = json :: to_ubjson(j);
// 0x7B,0x69、0x07、0x63、0x6F,0x6D,0x70、0x61、0x63、0x74、0x54、0x69、0x06、0x73、0x63、0x68、0x65、0x6D,0x61、0x69、0x00、0x7D
//往返
json j_from_ubjson = json :: from_ubjson(v_ubjson);
该库还支持BSON,CBOR(字节字符串)和MessagePack(bin,ext,fixext)的二进制类型。默认存储std::vector<std::uint8_t>为在库外部进行处理。
// CBOR字节串与有效载荷为0xCAFE
的std ::矢量<性病:: uint8_t > V = {的0x42, 0xCA, 0xFE的 };
//读取值
json j = json :: from_cbor(v);
// JSON值的类型为binary
j.is_binary(); //正确
//获取对存储的二进制值的引用
auto&binary = j.get_binary();
//二进制值没有子类型(CBOR没有二进制子类型)
binary.has_subtype(); //错误
//访问std :: vector <std :: uint8_t>成员函数
binary.size(); // 2
binary [ 0 ]; // 0xCA
binary [ 1 ]; // 0xFE
//将子类型设置为0x10
二进制。set_subtype( 0x10);
//序列化为MessagePack
auto cbor = json :: to_msgpack(j); // 0xD5(fixext2),0x10、0xCA,0xFE
支持的编译器
尽管已经是2020年,但对C ++ 11的支持仍然很少。当前,已知以下编译器可以工作:
- GCC 4.8-10.1(可能以后)
- 铛3.4-10.0(可能以后)
- Apple Clang 9.1-12.0(可能更高)
- 英特尔C ++编译器17.0.2(可能更高)
- Microsoft Visual C ++ 2015 /构建工具14.0.25123.0(以及更高版本)
- Microsoft Visual C ++ 2017 /生成工具15.5.180.51428(以及更高版本)
- Microsoft Visual C ++ 2019 /生成工具16.3.1 + 1def00d3d(以及更高版本)
很乐意了解其编译器/版本。
请注意:
- GCC 4.8有一个错误57824):多行原始字符串不能作为宏的参数。不要使用此编译器直接在宏中使用多行原始字符串。
- Android默认使用非常老的编译器和C ++库。要解决此问题,请将以下内容添加到中Application.mk。这将切换到LLVM C ++库,Clang编译器,并启用C ++ 11和其默认禁用的功能。
- APP_STL := c++_shared
- NDK_TOOLCHAIN_VERSION := clang3.6
- APP_CPPFLAGS += -frtti -fexceptions
该代码可使用Android NDK,修订版9-11(以及更高版本)和CrystaX的Android NDK版本10 成功编译。
- 对于在MinGW或Android SDK上运行的GCC 'to_string' is not a member of 'std',可能会发生错误(或类似的for strtod或strtof)。注意,这不是代码问题,而是编译器本身的问题。在Android上,请参见上文以使用较新的环境进行构建。对于MinGW,请参考此站点和此讨论以获取有关如何修复此错误的信息。对于Android NDK的使用APP_STL := gnustl_static,请参考此讨论。
- #error指令拒绝不支持的GCC和Clang版本。可以通过定义关闭JSON_SKIP_UNSUPPORTED_COMPILER_CHECK。请注意,在这种情况下,不希望获得任何支持。
Travis,AppVeyor,GitHub Actions和CircleCI当前在持续集成中使用编译器