decimal 屬性輸出 JSON 時指定的格式問題

這是之前專案遇到的狀況:輸出 金額 時只需處理到小數點下二位。既然是 金額,為了避免精準度造成的誤差都會選用 deciaml 資料類型,而在 db 中使用 money 儲存(因為業務需求面沒有運算需求可以使用,如果會有運算 money 有失真的風險,詳細內容請參考 欄位開立(2) - decimal, numeric, float, real, money 的抉擇),預設精準度為小數點下四位,為了符合目前系統的要求(小數點下二位),就需要調整輸出,來看看可以怎麼做吧

前提設定

  1. 自訂 model 中的 decimal 屬性

    public class TestData
    {
        public decimal Salary { get; set; }
    }
    
  2. 可能存在不同小數點位數的值

    var test = new TestData() { Salary = 1.03355M };
    var test2 = new TestData() { Salary = 2.0000M };
    var test3 = new TestData() { Salary = 3M };
    var test4 = new TestData() { Salary = 4.115M };
    
  3. 原始輸出

    #region - data1 -
    var test = new TestData() { Salary = 1.03355M };
    test.Dump();
    JsonConvert.SerializeObject(test).Dump();
    #endregion
                 
    #region - data2 -
    var test2 = new TestData() { Salary = 2.0000M };
    test2.Dump();
    JsonConvert.SerializeObject(test2).Dump();
    #endregion
        
    #region - data3 -
    var test3 = new TestData() { Salary = 3M };
    test3.Dump();
    JsonConvert.SerializeObject(test3).Dump();
    #endregion
                 
    #region - data4 -
    var test4 = new TestData() { Salary = 4.115M };
    test4.Dump();
    JsonConvert.SerializeObject(test4).Dump();
    #endregion
    

    1original

    可以看到除了整數(3)被加上一個小數位(3.0)之外,其他數值都會完整輸出

使用私有欄位儲存,在 get 時格式化

  • 加上私有欄位並格式化輸出

    public class TestData
    {
        private decimal _salary;
        public decimal Salary
        {
            get { return Math.Round(_salary, 2); }
            set { _salary = value; }
        }
    }
    
  • 結果

    2fieldformat

  • 缺點

    可以看到整數儲存與輸出 json 不符合,這個是 json.net 的特性,另外不一定可以完全符合指定小數位數

    4fieldissue

  • 改善

    如果有強烈的需求還是可以強制調整特定輸出,但這樣會有執行效率不佳的問題要特別留意

    public class TestData
    {
        private decimal _salary;
        public decimal Salary
        {
            get { return decimal.Parse(Math.Round((double)_salary, 2).ToString("0.00")); }
            set { _salary = value; }
        }
    }
    

    3fieldboxing

客製 Json.Net 的 JsonConverter

  1. 加入自訂 JsonConverter 並繼承 JsonConverter

    public class RoundingJsonConverter : JsonConverter
    {
        //指定精準度
        int _precision;
        //指定四捨五入的傾向
        MidpointRounding _rounding;
            
        //預設精準度小數點下 4 位
        public RoundingJsonConverter(): this(4)
        {
        }
            
        public RoundingJsonConverter(int precision) : this(precision, MidpointRounding.AwayFromZero)
        {
        }
                        
        public RoundingJsonConverter(int precision, MidpointRounding rounding)
        {
            _precision = precision;
            _rounding = rounding;
        }
                        
        public override bool CanRead
        {
            get { return false; }
        }
                        
        public override bool CanConvert(Type objectType)
        {
            return objectType == typeof(decimal);
        }
                        
        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            throw new NotImplementedException();
        }
                        
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            decimal _value=(decimal)value;
            writer.WriteValue(Math.Round(_value, _precision, _rounding));
        }
    }
    
  2. 在欲指定精準度的屬性上加入 attribute

    public class TestData
    {
        [JsonConverter(typeof(RoundingJsonConverter),2)]
        public decimal Salary { get; set; }
    }
    
  • 結果

    4jsonconverter

  • 缺點

    一樣有指定小數位數未生效的狀況,以下指定小數四位為例

    5jsonissue

  • 改善

    一樣會有效能問題

    public class RoundingJsonConverter : JsonConverter
    {
        //指定精準度
        int _precision;
        //指定四捨五入的傾向
        MidpointRounding _rounding;
            
        //預設精準度小數點下 4 位
        public RoundingJsonConverter() : this(4)
        {
        }
            
        public RoundingJsonConverter(int precision) : this(precision, MidpointRounding.AwayFromZero)
        {
        }
                        
        public RoundingJsonConverter(int precision, MidpointRounding rounding)
        {
            _precision = precision;
            _rounding = rounding;
        }
            
        public override bool CanRead
        {
            get { return false; }
        }
            
        public override bool CanConvert(Type objectType)
        {
            return objectType == typeof(decimal);
        }
            
        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            throw new NotImplementedException();
        }
            
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            decimal _value = (decimal)value;
            //為值補 0
            writer.WriteValue(decimal.Parse(Math.Round(_value, _precision, _rounding).ToString("0.".PadRight(2 + _precision, '0'))));
        }
    }
    

    6josnconvertboxing

心得

事實上專案中的介接目標系統沒有強制要求所有有數字都需精準至小數下二位,只是調整過程中潔癖發作,一直想要調整到人眼看也是很整齊,不過實在很難,最後還是選了個不漂亮的做法,當然主因就是找不到更好的方式XD,學藝不精,只好先紀錄一下日後有能力或是緣份到了再來改寫

參考資訊

  1. 欄位開立(2) - decimal, numeric, float, real, money 的抉擇
  2. Json.NET serializing float/double with minimal decimal places, i.e. no redundant “.0”?
  3. JSON轉換時去除小數字尾零