客製 Json.NET 的 JsonConverter - 自動 Initial Value Type 屬性

同事負責的專案原本使用 XML 做為資料傳遞的媒介,為了縮小網路傳輸封包,所以改用 json,而這個動作讓原本正常運行的功能出現問題

問題描述:使用 XML 在 Value Type 未給值時,程式會自動初始化才寫入 XML (這個部份是同事轉述,我沒有實際測試,說錯請指教),後來改用 json 後未給值的 Value Type 則是會直接使用 null 進行寫入,這讓接收端未檢查 property 是否 null 的程式出現 NullPointerException

第一個念頭:在接收端使用 Null 條件運算子(Null-conditional / Elvis operator - ?.),但要改的程式碼非常多,立馬放棄

第二個念頭:修改 Json.NET 的 JsonConverter,直接在寫入 json 前先進行初始化動作,最後就是這個方案雀屏中選了,就來看看該怎麼做吧

基本環境說明

  1. 自訂型別

    public class userData<T>
    {
        public string TestName { get; set; }
        public Guid id { get; set; }
        public List<T> name { get; set; }
        public Dictionary<string,int> TestDic { get; set; }
        //2017/07/10 同事反應需要 IList 加入
        public IList<T> TestIList { get; set; }
        //2017/07/10 同事反應需要 IList 順便測試 IEnumerable
        public IEnumerable<T> TestEnum { get; set; }
        public int[] Products{ get; set; }
    }
    
  2. 使用方式

    以下使用 LINQPad demo

    userData<string> user = new userData<string> { };
    user.Dump();
    JsonConvert.SerializeObject(user).Dump();
    

    1originuse

建立 class 繼承 JsonConverter

關於客製 JsonConverter 可以參考 Json.NET 的官方文件 Custom JsonConverter

public class InitialJsonConvert : JsonConverter
{
    private readonly Type[] _types;

    public InitialJsonConvert(params Type[] types)
    {
        _types = types;
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        JToken t = JToken.FromObject(value);

        if (t.Type != JTokenType.Object)
        {
            t.WriteTo(writer);
        }
        else
        {
            JObject o = (JObject)t;
            IList<string> propertyNames = o.Properties().Select(p => p.Name).ToList();

            o.AddFirst(new JProperty("Keys", new JArray(propertyNames)));

            o.WriteTo(writer);
        }
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        throw new NotImplementedException("Unnecessary because CanRead is false. The type will skip the converter.");
    }

    public override bool CanRead
    {
        get { return false; }
    }

    public override bool CanConvert(Type objectType)
    {
        return _types.Any(t => t == objectType);
    }
}

開始客製

  1. 使用 reflection 檢查內容

    // 取得傳入 class 型別
    Type type = value.GetType();
    // 取得傳 class 的 property 資訊
    PropertyInfo[] propInfos = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
    // 針對所有 property 資訊操作
    foreach (var element in propInfos)
    {
        // 檢查傳入值是否為 null
        if (element.GetValue(value) == null)
        {
            //取得 property 的型別
            Type pt = element.PropertyType;
            // string 是 value type 的特例,需要特別處理
            if (pt == typeof(string))
            {
                // 使用 string.Empty 初始化 property 
                element.SetValue(value, string.Empty);
            }
            // array 也需要特別處理
            else if (pt.IsArray)
            {
                var arrayType=pt.GetElementType();
                element.SetValue(value,Array.CreateInstance(arrayType, 0) );
            }
            //2017/07/10 同事反應需要 IList 加入
            else if(pt.IsInterface && type.IsGenericType)
            {
                var Ttype=pt.GetGenericArguments()[0];
                element.SetValue(value, Array.CreateInstance(Ttype,0));
            }
            else
            {
                // 使用 property 的型別來建立實體
                element.SetValue(value, Activator.CreateInstance(pt));
            }
        }
    }
    
  2. 完整程式碼

    public class InitialJsonConvert : JsonConverter
    {
        private readonly Type[] _types;
        public InitialJsonConvert(params Type[] types)
        {
            _types = types;
        }
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            // 取得傳入 class 型別
            Type type = value.GetType();
            // 取得傳 class 的 property 資訊
            PropertyInfo[] propInfos = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
            // 針對所有 class 的 property 資訊操作
            foreach (var element in propInfos)
            {
                // 檢查傳入值是否為 null
                if (element.GetValue(value) == null)
                {
                    //取得 property 的型別
                    Type pt = element.PropertyType;
                    // string 是 value type 的特例,需要特別處理
                    if (pt == typeof(string))
                    {
                        // 使用 string.Empty 初始化 property 
                        element.SetValue(value, string.Empty);
                    }
                    // array 也需要特別處理
                    else if (pt.IsArray)
                    {
                        var arrayType=pt.GetElementType(); 
                        element.SetValue(value,Array.CreateInstance(arrayType, 0) );
                    }
                    //2017/07/10 同事反應需要 IList 加入
                    else if(pt.IsInterface && type.IsGenericType)
                    {
                        var Ttype=pt.GetGenericArguments()[0];
                        element.SetValue(value, Array.CreateInstance(Ttype,0));
                    }
                    else
                    {
                        // 使用 property 的型別來建立實體
                        element.SetValue(value, Activator.CreateInstance(pt));
                    }
                }
            }
            JToken t = JToken.FromObject(value);
            if (t.Type != JTokenType.Object)
            {
                t.WriteTo(writer);
            }
            else
            {
                JObject o = (JObject)t;
                IList<string> propertyNames = o.Properties().Select(p => p.Name).ToList();
                        o.AddFirst(new JProperty("Keys", new JArray(propertyNames)));
                        o.WriteTo(writer);
            }
        }
        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            throw new NotImplementedException("Unnecessary because CanRead is false. The type will skip the converter.");
        }
        public override bool CanRead
        {
            get { return false; }
        }
        public override bool CanConvert(Type objectType)
        {
            return _types.Any(t => t == objectType);
        }
    }
    

如何使用

  1. 在 SerializeObject 時使用 Formatting 與自訂 JsonConvert

    以下使用 LINQPad demo

    JsonConvert.SerializeObject(user, Newtonsoft.Json.Formatting.Indented, new InitialJsonConvert(user.GetType())).Dump();
    
  2. 實際效果

    • 修改前後程式碼對比

      請使用 LINQPad 執行

      // demo 用 instance
      userData<string> user = new userData<string> { };
      //輸出 user
      user.Dump();
      //輸出 serialize 後的 user string
      JsonConvert.SerializeObject(user).Dump();
      // 輸出自訂 serialize 的 user string
      JsonConvert.SerializeObject(user, Newtonsoft.Json.Formatting.Indented, new InitialJsonConvert(user.GetType())).Dump();
      // 輸出經過自訂 serialize 的 user (value type 已被初始化)
      user.Dump();
      
    • 輸出結果

      2result

心得

我覺得應該有更簡單的做法,這個需求應該很常見才是,但因為是 production issue 時間緊迫,先求可以解決問題,有空再來找更好的解法,如果大家知道有哪個設定可以達成目的,拜託請告訴我,讓小弟學習一下,感謝

參考資訊

  1. Custom JsonConverter
  2. c# reflection getProperty and getValue
  3. How to check if a variable is Array or Object?
  4. How do I create a C# array using Reflection and only type info?
  5. Get a new object instance from a Type