【答客問】Json.NET-動態決定屬性是否序列化
18 |
昨天提到Json.NET屬性序列化設定,接獲讀者森哥留言:
請問黑大,
針對不需要序列化的「屬性」是否可以透過程式「動態」設定或是過濾?
有預感遲早也會遇到這個靠杯火盃的考驗,決定打鐵趁熱,馬上來練習。所幸,Json.NET真的很強大,早就料想到此一需求,提供ContractResolver以實現神乎奇技的高度動態化。
我寫了一個範例,展示兩種動態決定應序列化屬性的情境:
- Serialize時傳入屬性名稱陣列作為參數,正向表列JSON應包含的屬性。
- 由物件屬性值決定屬性是否要序列化,例如: 如果是女生就不包含年齡。(這幾乎已彈性到極點,雖然實務上不常用到)
程式的做法是宣告兩個繼承自DefaultContractResolver的類別: LimitPropsContractResolver在建構時傳入string[]參數列出要序列化的屬性名稱,並覆寫CreateProperties方法,過濾base.CreateProperties()傳回的IList<JsonProperty>,只保留前述string[]有列出的屬性;HideAgeContractResolver則覆寫CreateProperty()方法,由base.CreateProperty()取得JsonProperty,JsonProperty有個ShouldSerialize屬性可以傳入Lambda運算式,逐筆處理每個要序列化的物件,在Lambda運算式中可將物件轉型為原型別進行判斷,若不要序列化就傳回false。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Serialization;
namespace ConsoleApplication1
{
class Program
{
public enum Gender
{
Male, Female
}
public class Person
{
public string Name { get; set; }
[JsonConverter(typeof(StringEnumConverter))]
public Gender Gender { get; set; }
public int Age { get; set; }
public Person(string name, Gender gender, int age)
{
Name = name; Gender = gender; Age = age;
}
}
public class HideAgeContractResolver : DefaultContractResolver
{
//REF: http://james.newtonking.com/projects/json/help/index.html?topic=html/ContractResolver.htm
protected override JsonProperty CreateProperty(MemberInfo member,
MemberSerialization memberSerialization)
{
JsonProperty p = base.CreateProperty(member, memberSerialization);
if (p.PropertyName == "Age")
{
//依性別決定是否要序列化
p.ShouldSerialize = instance =>
{
Person person = (Person)instance;
return person.Gender == Gender.Male;
};
}
return p;
}
}
public class LimitPropsContractResolver : DefaultContractResolver
{
string[] props = null;
public LimitPropsContractResolver(string[] props)
{
//指定要序列化屬性的清單
this.props = props;
}
//REF: http://james.newtonking.com/archive/2009/10/23/efficient-json-with-json-net-reducing-serialized-json-size.aspx
protected override IList<JsonProperty> CreateProperties(Type type,
MemberSerialization memberSerialization)
{
IList<JsonProperty> list =
base.CreateProperties(type, memberSerialization);
//只保留清單有列出的屬性
return list.Where(p => props.Contains(p.PropertyName)).ToList();
}
}
static void Main(string[] args)
{
List<Person> list = new List<Person>();
list.Add(new Person("George", Gender.Male, 18));
list.Add(new Person("Mary", Gender.Female, 40));
//正常輸出
Console.WriteLine(JsonConvert.SerializeObject(
list, Formatting.Indented));
var settings = new JsonSerializerSettings();
//加上ContractResolver,正向表列哪些屬性要序列化
settings.ContractResolver =
new LimitPropsContractResolver("Name,Age".Split(','));
Console.WriteLine(JsonConvert.SerializeObject(
list, Formatting.Indented, settings));
//加上ContractResolver,依物件的屬性值動態決定要不要序列化
settings.ContractResolver = new HideAgeContractResolver();
Console.WriteLine(JsonConvert.SerializeObject(
list, Formatting.Indented, settings));
Console.ReadLine();
}
}
}
程式執行結果如下,共有三段輸出,第一段為正常版;第二段套用LimitPropsContractResolver("Name,Age".Split(',')),故JSON中只見Name及Age,Gender被隱藏;第三段套用了HideAgeContractResolver(),如結果所示,Mary的JSON內容不包含年齡,George則包含。
[
{
"Name": "George",
"Gender": "Male",
"Age": 18
},
{
"Name": "Mary",
"Gender": "Female",
"Age": 40
}
]
[
{
"Name": "George",
"Age": 18
},
{
"Name": "Mary",
"Age": 40
}
]
[
{
"Name": "George",
"Gender": "Male",
"Age": 18
},
{
"Name": "Mary",
"Gender": "Female"
}
]
演練完畢,內心激動澎湃,對Json.NET的景仰如淊淊江水,綿綿不絕~
如果奧斯卡有最佳元件獎,我提名它!
Comments
# by 森哥
感謝黑大的解答,小弟對您的景仰如滔滔江水...
# by JRyo
黑暗大你好 想問一下 已經在class上 加上了 JsonIgnore 能否在序列化時 動態設定是否需要讀取 json Attbuite? (在功能面上 會用到同一個class做兩個序列化 一個是完整序列化 一個是為了縮短json字串 所以刪減物件的內容 而因為物件包含的屬性太多樣 與多層 所以直接寫在物件上面 ) 有到json.net尋找另外寫 contractResolver的方式 http://www.newtonsoft.com/json/help/html/ConditionalProperties.htm 但是 ShouldSerializeContractResolver 卻找不到class與引用 能否幫忙解惑 感謝
# by Jeffrey
to JRyo, 問一下,序列化時有JsonConvert.SerializeObject(book, Formatting.Indented, new JsonSerializerSettings { ContractResolver = new ShouldSerializeContractResolver() }); 嗎? 參考: http://goo.gl/49Vo17
# by JRyo
Hi 黑暗大 感謝回復 抱歉 我說明一下 在Setting 寫入 ContractResolver的部分沒有問題 另外 實作 ShouldSerializeContractResolver 我發現我寫錯的地方了 但是 不確定在 ContractReolver 內該用甚麼方法 取消掉 JsonProperty 的 jsonignore 這個問題才是我真的想要問的 public class a { [jsonignore] int intA {get;set;} } 如上 是否在 json Setting 內 已有方法或類別可以控制 或是 要用 JsonProperty 的哪個屬性 讓我套用Contract時可以選擇忽略 ignore 的attribute 另外在請問一下 在一個Setting內 只能寫入一個 ConTractResolver嗎 如果我有兩個規則 是否只能選擇一個用
# by Jeffrey
to JRyo, 偵測Property有沒有加[JsonIgnored]可以使用PropertyInfo.GetCustomAttribute() 參考:http://stackoverflow.com/a/6637710/288936 。至於ContractResolver應該只能指定一個,若想綜合多個ContractResolver的邏輯,可以寫一個整合版,在其中依不同狀況執行不同邏輯。
# by JRyo
Hi 黑暗大 感謝回復 所以看起來 還是要讓他在序列化時一個一個處理的樣子 我會去研究看看 感謝給予資源 謝謝
# by Ricky
黑大您好 請問如果是要過濾掉一個List裡面的object 該如何下手呢? 謝謝~
# by Jeffrey
to Ricky, 範例中有一段寫法: if (p.PropertyName == "...") { p.ShouldSerialize = true or false (動態決定是否要序列化) } 是否適用?
# by Ricky
黑大您好 嘗試以後發現p.PropertyName是隱藏屬性沒辦法隱藏物件 我現在的情境是: 在一個List的物件當中,同一個圖層(Layer)中,有許多個圖形,包括圓形(Circle)、多邊形(Polygon),這些圖形有座標等資訊 我想要序列化的時候,把所有type為Circle的隱藏起來 範例如下: "Objects": [ { "Class": "A", "Name": "Image1", "Layers": [ { "DrawMode": 0, "Shape": { "$type": "Circle", "Center": "667.126739375705,127.059044753667", "Radius": 0.0, "RadiusX": 86.377240728513485, "RadiusY": 86.377240728513485 } }, { "DrawMode": 0, "Shape": { "$type": "Polygon", "Point0": "1243.0011282437,308.048138397894", "Points": [ "976.453553967657,452.839413313276", "1479.93230537796,847.724708537044", "1664.21210981572,535.107183151561" ] } }, { "DrawMode": 1, "Shape": { "$type": "Circle", "Center": "226.171493042497,502.200075216247", "Radius": 0.0, "RadiusX": 146.94414717221008, "RadiusY": 146.94414717221008 } } ] } ] 如果我是用if (p.PropertyName == "Layers") 這樣變成隱藏Layers屬性而不是隱藏含有Circle的物件 麻煩黑大給個指引~ 謝謝
# by Jeffrey
to Ricky,能在 Github 放個簡單專案展示你目前試作的版本嗎?我可以試著改寫看看。
# by Ricky
黑大您好 小弟簡單弄個專案上傳到Github了 https://github.com/RickyChen1216/JsonNETHideObjectTest json的範例檔也有推上去 另外DynamicContractResolver裡面是自己嘗試但一樣無法隱藏物件的,我先註解掉了 麻煩黑大看看了 有問題再跟我說 謝謝!
# by Jeffrey
to Ricky, 我看這個問題的關鍵反而是抽象類別的反序列化,試著用 JsonConverter 解決,剔除 MCircle 的部分簡單用 LINQ 處理。已丟了 PR,請參考。 若不介意,可借用這個案例分享到我的部落格嗎?
# by Ricky
黑大您好 原來關鍵是抽象類別的反序列化,這部分我還得多加研究才行,感謝黑大幫忙突破盲腸 案例當然樂意提供給黑大分享,又多學了一課! 最後因為我想把Objects裡面的物件中,Layer為空的物件也一併移除 所以return前加上了annotationViewModel.Objects.Where(o => o.Layers.Count() < 1).ToList().ForEach(o => annotationViewModel.Objects.Remove(o)); json就變成: { "Objects": [ { "Class": "A", "Name": "image1", "Layers": [ { "DrawMode": 0, "Shape": { "Point0": "1243.0011282437,308.048138397894", "Points": [ "976.453553967657,452.839413313276", "1479.93230537796,847.724708537044", "1664.21210981572,535.107183151561" ] } }, { "DrawMode": 1, "Shape": { "P0": "565.114704776231,725.968409176382", "P1": "960,1068.20233170365" } } ] } ] } 完全達到我想要的結果,感謝黑大的幫忙!
# by Ricky
不過如果今天反過來是要存成json 原理一樣嗎? 謝謝黑大
# by Ricky
啊 黑大 我發現json裡面的Shape裡面的 "$type": "Polygon" 這行不見了 有辦法寫進去嗎? 謝謝
# by Ricky
黑大您好 我試著改寫WriteJson public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { JObject o = JObject.FromObject(value); if (o.Type == JTokenType.Object) { if (value.GetType() != typeof(MMultiPolygon)) { var assemblyQualifiedName = value.GetType().AssemblyQualifiedName; var words = assemblyQualifiedName.Split(','); string type = words[0] + "," + words[1]; o.AddFirst(new JProperty("$type", type)); o.WriteTo(writer); } } } 但這樣始終沒辦法在序列化時去刪除Layer.shape為空的物件 有什麼辦法嗎? 謝謝~
# by Jeffrey
to Ricky,我上週有再發了 PR,發現 TypeNameHandling = TypeNameHandling.Auto 即可處理,先前的做法有點繞遠路,你參考看看。
# by Ricky
謝謝黑大 讀檔部分似乎沒什麼問題 不過寫檔部分我後來是在Objects的屬性上加上一個Converter去針對每個Object來刪除物件 再寫入檔案內 感謝提供靈感!!