Newtonsoft.Json
介绍
-
Newtonsoft.Json 另一个名称是 json.net
-
官网 https://www.newtonsoft.com/json
-
github https://github.com/JamesNK/Newtonsoft.Json
-
文档 https://www.newtonsoft.com/json/help/
-
示例 https://www.newtonsoft.com/json/help/html/Samples.htm
-
由于很多库都会用到 Newtonsoft.Json,而 unity 中不能有同名程序集,因此 unity 集成了自己的 Newtonsoft.Json 插件,
可在 PackageManager 中查找 collab-proxy 安装,命名空间从 Newtonsoft.Json 改成 Unity.Plastic.Newtonsoft.Json
建议你写的代码都使用这个插件,对于多个第3方都使用 Newtonsoft.Json,只能保留1个,强制删除多余的
安装
- 直接通过 unity 的 PackageManager 安装 collab-proxy
- 通过官网下载,里面包含源码和编译好的程序集
- 通过 github 克隆源码后自己编译
通过类似DOM序列化JSON
-
类层次介绍
- JToken 对象基类,可以放在 : 右边的,都是 JToken,包含类型和值
- JProperty 表示一组 name:jtoken,有一个字符串的 Name 和 一个 JToken 的Value
- JValue : JToken 原生值类型,比如 int,string 都会创建 JValue 来存放,原生值类型跟JToken可以隐式转换,会自动创建 JValue
如果是原生类型,,则可以跟 JToken 直接互转,比如 int a = (int)jtoken; JToken jtoken=(JToken)a;
如果是派生类型,一样可以互转包含原生类型,以及 JObject , JArray 等派生类型 - JObject:JToken 表示 {},包含一组 JProperty
- JArray:JToken 表示 [],包含一组 JToken
-
示例代码
public class Example { public static string JSON_OBJECT_STR = "{\"ID\":11,\"Name\":\"张三1\",\"Birthday\":\"2020-11-02T00:00:00\",\"IsVIP\":true}"; // \" 表示 " public static string JSON_ARRAY_STR = @"[{""ID\"":11,""Name"":""张三1""},{""ID"":12,""Name"":""李四""}]"; // 用 @ 转义,则用 "" 表示 " public static void RemoveProperty(JObject obj) { if ( obj.Remove("ID") ) { } } // 创建对象 public static JObject CreateObject() { JObject obj = new JObject(); obj.Add("ID", 11); obj.Add("Name", "张三1"); obj.Add("Birthday", DateTime.Parse("2020-11-02")); obj.Add("IsVIP", true); obj.Add("Account", 13.34f); obj.Add("Remark", null); obj.Add(new JProperty("ID", 11)); } // 用初始化器填充对象 public static JObject CreateObject2() { return new JObject() { { "name", "jack" }, { "age", 28 } }; } public static JObject CreateObject3() { return new JObject( new JProperty("title", "James Newton-King"), new JProperty("price", 3.3), new JProperty("channel", new JObject( new JProperty("item", new JArray( from p in posts orderby p.Title select new JObject( new JProperty("title", p.Title), new JProperty("description", p.Description), ) )) )) ); } public static JObject ParseObject1() { return JObject.Parse(JSON_OBJECT_STR); } public static JObject ParseObject2() { // 实体类直接转成 JObject Person person = new Person(); return JObject.FromObject(person); } // 复用匿名对象快速创建 JObject public static JObject CreateObject5() { // 匿名实体类 return JObject.FromObject(new { name = "jack", age = 28, data = new {title="1"} }); } public static JArray CreateArray() { JArray array = new JArray(); array.Add(new JValue("上班")); array.Add(new JValue("下班")); return array; } public static JArray CreateArray2() { return new JArray("上班", "下班"); } public static JArray CreateArray3() { return JArray.Parse(JSON_ARRAY_STR); } public static JArray CreateArray4() { Person[] person = new Person[2]; return JArray.FromObject(person); } public static void WriteJsonFile() { JObject o = CreateObject(); // 直接写入文件 using (StreamWriter writer = File.CreateText("c:\\1.json")) { o.WriteTo(new JsonTextWriter(writer)); } // 转成字符串再写入文件 string s = o.ToString(); File.WriteAllText("c:\\1.json", s); } // 解析 JToken public static void ParseToken(JToken token) { if ( token == null ) return; // 如果你知道类型,直接强转,对于值类型,已经重载了强转函数 int i = (int)token; string s = (string)token; JObject o = (JObject)token; JArray a = (JArray)token; // 如果不知道类型,可以通过类型判断 if ( token.Type == JTokenType.Object ) {} } // 解析 JObject public static void ParseObject(JObject obj) { // 几种获得属性的方法,底层实现都一样 JToken j1 = obj["ID"]; // 获得属性 "ID" 的值 JToken j2 = obj.Property("ID"); JToken j3 = obj.GetValue("ID"); if ( obj.TryGetValue(out JToken j4) ){} // 是否包含属性 if ( obj.ContainsKey("ID") ) {} // 遍历属性 foreach (JProperty prop : obj.Properties()) { string name = prop.Name; JToken value = prop.Value; } // 直接遍历属性的值 foreach (JToken j3 : obj.PropertyValues()) { } ParseToken(j1); } public static void ParseArray(JArray array) { // 遍历 foreach (JToken jtoken in array) {} foreach (JToken jtoken in array.Children() ) {} for ( int i=0; i<array.Count; i++ ) { JToken jtoken = array[i]; } // 直接转成具体类型 foreach ( int i in array.Values<int>() ) {} } public static void ReadJsonFile() { // 直接解析文件 using (StreamReader reader = File.OpenText(@"c:\person.json")) { JObject o = (JObject)JToken.ReadFrom(new JsonTextReader(reader)); ParseObject(o); } // 读取字符串后再解析 string s = File.ReadAllText(@"c:\person.json"); { JObject o = (JObject)JToken.Parse(s); ParseObject(o); } } }
通过反射序列化json
-
跟实体类转换
public class Product { // "Name": "Apple" public string Name; // "ExpiryDate": "2008-12-28T00:00:00" public DateTime ExpiryDate; // "Price": 3.99 [DefaultValue(30)] // 设置默认值,正常情况,对象默认值是 null,值类型默认值就是该类型默认初始值(不是该变量的初始值,比如 int 默认值是 0),配合 DefaultValueHandling.Ignore 可以减少 json 文件大小 public float Price; // "Sizes": ["Small", "Medium", "Large"] public string[] Sizes; // "MyMemo":"meno" [JsonProperty("MyMemo"){NullValueHandling = NullValueHandling.Ignore, IsReference = true}] // 可以定制名称,为null时不序列化,多个变量指向同一对象时保持引用,等还有很多其它选项 public string Memo; [JsonProperty] // 让私有成员也能序列化 private string title; [JsonIgnore] // 不序列化 public string content; // "ExpiryDate":new Date(1230375600000) [JsonConverter(typeof(JavaScriptDateTimeConverter))] // 修改序列化格式 public DateTime JSDate; [JsonConverter(typeof(StringEnumConverter))] //默认:枚举对应的整型数值 加上:枚举对应的字符,也就是现在可以用整数,也可以用字符串,输出时是字符串 public NullValueHandling NullValueHandling { get; set; } [JsonExtensionData] // 所有未识别的元素会加入这里,转成 json 时会原样输出 private IDictionary<string, JToken> _additionalData; public string Domain { get; set; } public Product Owner; // 可以增加一个函数来动态决定某个属性是否需要序列化,函数签名为 bool ShouldSerialize{属性或字段名}(); bool ShouldSerializeOwner() { return Owner != this; } public Product() { _additionalData = new Dictionary<string, JToken>(); Sizes = new string[]{"Default"}; // 反序列化时默认直接使用已有 Sizes 对象,会在后面追加元素,除非设置 ObjectCreationHandling.Replace,则会重新创建 Sizes 对象 } [JsonConstructor] // 反序列化时默认用无参构造函数,你可以指定用带参数构造函数,会用 Name 属性做为参数 public Product(string name) { Name = name; } // 跟实现 ISe // OnSerializing 序列化前调用的函数 // OnSerialized 序列化后调用的函数 // OnDeserializing 反序列化前调用的函数 [OnDeserialized] // 反序列化后调用的函数 private void OnDeserialized(StreamingContext context) { // 比如可以在反序列化后,从没有处理的属性 SAMAccountName 中解析出 Domain string samAccountName = (string)_additionalData["SAMAccountName"]; Domain = samAccountName.Split('\\')[0]; } [OnError] // 错误处理 internal void OnError(StreamingContext context, ErrorContext errorContext) { errorContext.Handled = true; // 处理完要设置成 true } } public class Book : Product {} // 定制反序列化时生成派生类,CustomCreationConverter 是 JsonConverter 的派生类 // 只能固定转成某个派生类,不能根据类型动态转成不同的派生类,需要动态转换参考 TypeNameHandling public class ProductConverter : CustomCreationConverter<Product> { public override Product Create(Type objectType) { // objectType 可以是 Product 或其派生类 return new Book(); } } // 定制序列化时只序列化某些属性 public class DynamicContractResolver : DefaultContractResolver { private readonly char _startingWithChar; public DynamicContractResolver(char startingWithChar) { _startingWithChar = startingWithChar; } protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization) { IList<JsonProperty> properties = base.CreateProperties(type, memberSerialization); // 只序列化以 _startingWithChar 字符开头的属性 properties = properties.Where(p => p.PropertyName.StartsWith(_startingWithChar.ToString())).ToList(); // 动态决定某个属性是否要序列化,跟上面的 ShouldSerializeOwner 是一样的效果,2者选其一就行 if (property.DeclaringType == typeof(Product) && property.PropertyName == "Owner") { property.ShouldSerialize = instance => { Product e = (Product)instance; return e.Owner != e; }; } return properties; } } // 加了 OptIn ,则只有加 JsonProperty 的成员才会序列化 // 默认是 OptOut,所有公有成员会被序列化(包括字段和属性) // 还有一个是 Fields,所有字段会被序列化(包括公共和私有) [JsonObject(MemberSerialization.OptIn)] public class Item { public string Name; // 没有加 JsonProperty,不会被序列化 public int Age; } public class Example { Product TEST_PRODUCT = new Product(){ Name = "Apple", ExpiryDate = new DateTime(2008, 12, 28), Price = 3.99M, Sizes = new string[] { "Small", "Medium", "Large" } }; // 写入文件 public static void WriteJsonFile() { // 直接写入文件 JsonSerializer serializer = CustomSerializer(); using (StreamWriter sw = new StreamWriter(@"c:\json.txt")) using (JsonWriter writer = new JsonTextWriter(sw)) { serializer.Serialize(writer, product); } // 转成字符串后写入文件 string json = JsonConvert.SerializeObject(TEST_PRODUCT, Formatting.Indented, CustomSettings()); File.WriteAllText("c:\\1.json", json); // 输出 {"key1":"value1", "key2":"value2"} string dics = JsonConvert.SerializeObject(new Dictionary<string, string>(){{"key1":"value1"},{"key2":"value2"}}); } // 读取文件 public static void ReadJsonFile() { // 直接解析文件 JsonSerializer serializer = CustomSerializer(); using (StreamReader sr = new StreamReader(path, Encoding.Default)) { Product book = serializer.Deserialize<Product>(sr); } // 读取字符串后再解析 string json = File.ReadAllText("c:\\1.json"); // 实际返回的是 Book 对象,如果有多种类型要替换,就加入多少 JsonConverter Product book = JsonConvert.DeserializeObject<Product>(json, new ProductConverter(), new XXXConverter()); } // 跟 JObject 转换 public static void ConvertWithObject() { // 实体类转 JObject JObject o = JToken.FromObject(TEST_PRODUCT); // JObject 转 实体类 Person p = o.ToObject<Person>(); } // 使用 json 更新实体 public static void UpdateObject() { string json = @"{ 'Name': 'Apple', 'Price':1, 'Sizes':['Big'] }"; // 用 json 字符串中的值更新 TEST_PRODUCT 对象,对于数组会追加 // 结果: // Name = "Apple", // Price = 1, // Sizes = new string[] { "Small", "Medium", "Large", "Big" } JsonConvert.PopulateObject(json, TEST_PRODUCT); } // 定制序列化选项 public static JsonSerializer CustomSerializer() { JsonSerializer serializer = new JsonSerializer(); serializer.Converters.Add(new JavaScriptDateTimeConverter());// 时间转成js时间格式,"ExpiryDate": "2008-12-28T00:00:00" 变成 "ExpiryDate":new Date(1230375600000) serializer.NullValueHandling = NullValueHandling.Ignore; // null 值不做处理 // 还有很多可定制化选项,参考后面的 JsonSerializer 定制参数 // 也可以参考下一个函数 CustomSettings, 2者很多参数相机 return serializer; } // 定制序列化选项,JsonSerializerSettings 拥有跟 JsonSerializer 差不多的参数 // 只不过 JsonSerializerSettings 是给 JsonConvert.SerializeObject 用的 public static JsonSerializerSettings CustomSettings() { JsonSerializerSettings jsetting = new JsonSerializerSettings() { NullValueHandling = NullValueHandling.Ignore, // null 值不做处理 Error = delegate(object sender, ErrorEventArgs args) // 错误处理程序,如果某个元素解析出错,该元素和所有祖先节点都会调用一次错误处理程序 { errors.Add(args.ErrorContext.Error.Message); args.ErrorContext.Handled = true; // 不想被后面的处理,就要设置已捕获 }, Converters = {new JavaScriptDateTimeConverter()}, // 时间转成js时间格式,"ExpiryDate": "2008-12-28T00:00:00" 变成 "ExpiryDate":new Date(1230375600000) PreserveReferencesHandling = PreserveReferencesHandling.Objects, // 如果有2个变量指向同一对象,则默认会序列化2份相同的数据,此时反序列化后会变成2个对象,有时候可能不满足需求,还可能造成循环引用 // 设置该标志后,会在第一次序列化对象时加个属性"$id",比如 "Person" : {"$id": "1", "name":"n"},后续序列化该对象时变成 "Person" : {"$ref","1"} // 1是自动生成的对象编号,这样反序列化后就会指向同一对象 // 在特性 [JsonObject], [JsonArray] 和 [JsonProperty].中加入 IsReference = true,可以定制这些对象默认开启保持引用 DefaultValueHandling = DefaultValueHandling.Ignore, // 忽略默认值,可以减少json文件大小,这里的默认值不是字段初始时的值,而是类型初始值,比如 int 初始就是 0,可以用 [DefaultValue(默认值)] 特性修改某些属性的默认值不为该类型的默认值 ContractResolver = new DynamicContractResolver('A'), // 只序列化以字符 A 开头的属性 TraceWriter = new MemoryTraceWriter(), // 调试日志,你也可以自己实现 ITraceWriter,一般用默认的 MemoryTraceWriter 就够了 TypeNameHandling=TypeNameHandling.Auto, // 是否自动存储类型信息,会自动增加 "$type" 属性,默认是None,对于派生类体系很有用,可设置为 Auto,当类型不符时自动存储类型信息 ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor, // 允许使用非公有默认构造函数 ObjectCreationHandling = ObjectCreationHandling.Replace, // 反序列化时对已有对象怎么处理,Reuse(默认) 直接使用,Replace 替换,对于复杂对象很关键,比如 List 默认用 Reuse ,则在后面追加对象,改用 Replace 后变成先清空已有对象 MissingMemberHandling = MissingMemberHandling.Error, // 反序列化时,缺少成员当做错误处理,默认是 Ignore 直接忽略 ReferenceLoopHandling = ReferenceLoopHandling.Ignore, // 循环引用如何处理,默认 Ignore MetadataPropertyHandling = MetadataPropertyHandling.ReadAhead, // 默认 $ 开头的内置属性(比如 $id $type)只能放在其它属性前面,设置成 ReadAhead 后也可以放在后面,但效率会降低,一般没必要 }; return jsetting } // 只反序列化部分对象 public static void DeserializePartialJson() { // 对象这样转换 JObject jo = JObject.Parse(@"'data':{'name':'qml'}"); JToken jdata = jo["data"]; MyData = jdata.ToObject<MyData>(); // 数组这样转换 JObject jo = JObject.Parse(@"'data':['1','2']"); IList<JToken> jresults = jo["data"].Children().ToList(); IList<string> results = new List<SearchResult>(); foreach (JToken jresult in jresults) { string result = jresult.ToObject<string>(); results.Add(result); } } } -
JsonSerializer 定制参数
使用 JsonSerializer 可以全面精细的控制序列化流程,下面是可以定制的属性- DateFormatHandling
定制日期序列化 - MissingMemberHandling
定制字段缺失处理 - ReferenceLoopHandling
循环引用处理,比如自己的成员引用自己 - NullValueHandling
空值处理 - DefaultValueHandling
默认值处理 - ObjectCreationHandling
创建对象 - TypeNameHandling
是否序列化类型名称 - TypeNameAssemblyFormat
使用简单类型名还是完整类型名 - SerializationBinder
自定义类型到类型名称的转换 - MetadataPropertyHandling
决定metadata 属性的读取顺序,$type $id 这种字段叫做 metadata 属性 - ConstructorHandling
决定创建对象时如何构造对象 - Converters
自定义转换过滤器 - ContractResolver
自定义序列化某种类型对象 - TraceWriter
关联日志调试器 - Error
错误捕获器
- DateFormatHandling
嵌套派生类体系的序列化
// 派生类体系,下面示例共用
class Address
{
public string address;
public int number;
public virtual string Print()
{
return $"{address},{number}";
}
}
class Address1 : Address
{
public int order;
public float price;
public override string Print()
{
return $"{address},{number},{order},{price}";
}
}
class Address2 : Address
{
public string title;
public string content;
public override string Print()
{
return $"{address},{number},{title},{content}";
}
}
-
把派生类序列化成json字符串,存储在某个字段
逻辑简单,但json序列化的字符串不好阅读,可用方法2替代class Person { public int age; public string data; // 派生类序列化的json public int dataType; // 派生类类型 } class Main { public static void Main() { Address1 a = new Address1() { address = "a/b/c", number = 112, order=110, price=1.112f }; Person b = new Person() { age = 10, data = JObject.Parse(JsonConvert.SerializeObject(a)), dataType=1}; using (StreamWriter sw = File.CreateText("d:\\1.json")) { // 序列化成字符串 string json = JsonConvert.SerializeObject(b, Formatting.Indented); sw.Write(json); } using (StreamReader sr = new StreamReader("d:\\1.json", Encoding.Default)) { // 反序列化 Person newb = JsonConvert.DeserializeObject<Person>(sr.ReadToEnd()); if ( newb.dataType == 1) { Address1 newa = JsonConvert.DeserializeObject<Address1>(newb.data); Console.WriteLine($"newa={newa.address},{newa.number},{newa.order},{newa.price} newb={newb.age},{newb.data}"); } } } } -
把派生类序列化成 JToken,存储在某个字段
(推荐)逻辑简单,且方便阅读class Person { public int age; public JToken? data; // 派生类序列化的 JToken public int dataType; // 派生类类型 [JsonIgnore] public Address address; [OnDeserialized] void OnDeserialized() { // 动态转成派生类 if ( dataType == 1 && data != null ) address = data.ToObject<Address1>(); } } class Main { public static void Main() { Address1 a = new Address1() { address = "a/b/c", number = 112, order=110, price=1.112f }; Person b = new Person() { age = 10, data = JObject.FromObject(a), dataType=1}; using (StreamWriter sw = File.CreateText("d:\\1.json")) { // 序列化成字符串 string json = JsonConvert.SerializeObject(b, Formatting.Indented); sw.Write(json); } using (StreamReader sr = new StreamReader("d:\\1.json", Encoding.Default)) { // 反序列化 Person newb = JsonConvert.DeserializeObject<Person>(sr.ReadToEnd()); Address1 newa = newb.address; Console.WriteLine($"newa={newa.address},{newa.number},{newa.order},{newa.price} newb={newb.age},{newb.data}"); } } } -
利用newtonsoft.json自带的存储对象类型功能
(推荐)全自动处理,缺点是如果重构修改了类型名称,会导致已有json解析不了,而且不适合跟服务端通信,适用场景是保存运行时动态配置public class Person { public int age; public Address data; } class Main { public static void Main() { Address1 a = new Address1() { address = "a/b/c", number = 112, order=110, price=1.112f }; Person b = new Person() { age = 10, data = a}; using (StreamWriter sw = File.CreateText("d:\\1.json")) { // 设置 TypeNameHandling.Auto 后,如果字段类型和对象实际类型不一致,则会增加 "$type"="类名,程序集名" 属性记录对象实际类型 string json = JsonConvert.SerializeObject(b, Formatting.Indented, new JsonSerializerSettings() { TypeNameHandling=TypeNameHandling.Auto}); sw.Write(json); } using (StreamReader sr = new StreamReader("d:\\1.json", Encoding.Default)) { // 反序列化 Person newb = JsonConvert.DeserializeObject<Person>(sr.ReadToEnd(), new AddressConverter()); Address1 newa = (Address1)newb.data; Console.WriteLine($"newa={newa.address},{newa.number},{newa.order},{newa.price} newb={newb.age},{newb.data}"); } } } -
利用JsonConverter定制转换器
(推荐)// 自定义转换器 public class AddressConverter : JsonConverter { bool m_canRead = true; bool m_canWrite = true; public override bool CanRead => m_canRead; public override bool CanWrite => m_canWrite; public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { // 调用下面这2个函数均会造成死循环,会嵌套调用 WriteJson, writer 参数不一样,其它参数一样 // serializer.Serialize(writer, value); // JObject address = JObject.FromObject(value,serializer); // 不传 serializer 就不会死循环,但序列化配置也同时恢复成默认 // 直接调用 FromObject 会造成死循环,重载 CanWrite 在调用前先禁用该转换器,这样就不会死循环 m_canWrite = false; JObject address = (JObject)JObject.FromObject(value,serializer); m_canWrite = true; if ( value is Address1 ) { address["AddressType"] = 1; } else { address["AddressType"] = 2; } address.WriteTo(writer, serializer.Converters?.ToArray()); // 常用的还有以下方法进行处理 // 1. 直接拼接 // var vec = (Vector2)value; // writer.WriteStartObject(); // 对象开始 // writer.WritePropertyName("x"); // 属性和值之间不需要其它东东 // writer.WriteValue(vec.x); // writer.WritePropertyName("y"); // writer.WriteValue(vec.y); // writer.WriteEndObject(); // 对象结束 } public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { // 调用下面这2个函数均会造成死循环,会嵌套调用 ReadJson, objectType 参数不一样,其它参数一样 // serializer.Deserialize(reader, objectType); // JObject jsonObject = JObject.Load(reader); // JObject address = jsonObject.ToObject(objectType,serializer); // 不传 serializer 就不会死循环,但序列化配置也同时恢复成默认 try { var jsonObject = JObject.Load(reader); Type t = null; if (jsonObject.TryGetValue("AddressType", out JToken addressType)) { switch ((int)addressType) { case 1: // Address1 t = typeof(Address1); break; case 2: // Address2 t = typeof(Address2); break; } } // 直接调用 FromObject 会造成死循环,重载 CanRead 在调用前先禁用该转换器,这样就不会死循环 m_canRead = false; object target = jsonObject.ToObject(t, serializer); m_canRead = true; // 常见的还有以下方法进行处理 // 1. 使用 Populate // var target = new Address1(); // serializer.Populate(jsonObject.CreateReader(), target); // 2. 直接拼接 // // 没 Read 前,TokenType=StartObject,也就是 { // reader.Read(); // 第1次Read,TokenType=PropName , Value="x" // reader.Read(); // 第2次Read,TokenType=Float // float x = reader.Value; // reader.Read(); // 第3次Read,TokenType=PropName , Value="y" // reader.Read(); // 第4次Read,TokenType=Float // float y = reader.Value; // reader.Read(); // 第5次Read,TokenType=EndObject,也就是 }, // 也就是说对象不用读取{,但要读取},这主要是跟普通值处理流程一致,普通值外部已经调用完 Read,在 ReadJson 函数中直接使用 reader.Value return target; } catch (Exception ex) { throw new Exception("解析异常:" + ex.Message); } } public override bool CanConvert(Type objectType) { return typeof(Address).IsAssignableFrom(objectType); } } -
总结
自己写自己读的场景中优先使用方案3
跟别人通信的情况下(跟服务端通信或读取策划配置表),方案2和方案4都可以采用,方案2更简单点,如果类型字段记录在外部,则只能用方案2
方案1不推荐,可用方案2替代
本文详细介绍了在Unity中如何使用Newtonsoft.Json,包括安装方法、通过DOM和反射序列化JSON,以及处理嵌套派生类体系的序列化。强调了在不同场景下选择合适序列化方式的重要性,并推荐了使用JsonConverter定制转换器。
1万+

被折叠的 条评论
为什么被折叠?



