将十进制数序列化为JSON,如何四舍五入?

35

我有一个类

public class Money
{
    public string Currency { get; set; }
    public decimal Amount { get; set; }
}

我想将其序列化为JSON格式。如果我使用JavaScriptSerializer,我会得到下面的结果:

{"Currency":"USD","Amount":100.31000}
由于API的要求,我需要提供最多两位小数的JSON数据。我认为应该有可能修改JavaScriptSerializer序列化十进制字段的方式,但我不知道如何实现。构造函数中可以传递SimpleTypeResolver,但据我所知它只对类型起作用。通过RegisterConverters(...)可以添加JavaScriptConverter,但似乎只适用于Dictionary。 我希望能够得到...
{"Currency":"USD","Amount":100.31}

我序列化后需要进行四舍五入,但不能将类型更改为双精度数。有没有人知道如何做到这一点?也许有一种替代JavaScriptSerializer,可以更详细地控制序列化过程。


你是否只想在序列化时将金额四舍五入?你可以向类中添加另一个属性,并将其序列化,而不是使用Amount属性,并标记原始的Amount属性不被序列化。 - Mark Sherretta
@MarkSherretta 是的,我只想在序列化为JSON时进行四舍五入。 我可以在不将其转换为字符串(“Amount”:“100.31”)的情况下这样做吗? 用于序列化的四舍五入双精度字段? - Halvard
最好只在一个地方进行更改。在我的情况下,对于所有被序列化的小数(不仅仅是在此对象中),这样做是可以的。 - Halvard
4个回答

23

到目前为止,我对实现这个功能的所有技术都不是完全满意。JsonConverterAttribute 似乎是最有前途的选择,但我无法接受硬编码参数和为每种选项组合创建转换器类的增加。

因此,我提交了一个PR,添加了向 JsonConverter 和 JsonProperty 传递各种参数的功能。它已被上游接受,并且我希望它将在下一个版本中发布(即 6.0.5 之后的版本)。

然后您可以按照以下方式进行操作:

public class Measurements
{
    [JsonProperty(ItemConverterType = typeof(RoundingJsonConverter))]
    public List<double> Positions { get; set; }

    [JsonProperty(ItemConverterType = typeof(RoundingJsonConverter), ItemConverterParameters = new object[] { 0, MidpointRounding.ToEven })]
    public List<double> Loads { get; set; }

    [JsonConverter(typeof(RoundingJsonConverter), 4)]
    public double Gain { get; set; }
}

请参考CustomDoubleRounding()测试,以获得一个示例。


我无法在版本8中使其工作。是否有一个完整的示例来说明如何使其工作?代码可以运行,但不会四舍五入任何东西。 - NickG
哦,刚意识到反序列化时它不起作用 - 只有在创建序列化为新的JSON时才有效 :( - NickG
尝试将CanRead更改为返回true,然后实现一个适当的ReadJson。我想不出任何理由,这不能很容易地双向实现。 - BrandonLWhite
谢谢 - 我会尝试一下的。我不确定是什么导致它使用ReadJSON函数,但现在你已经说了,我想我应该注意到了 :) - NickG
这是我找到的最佳答案,该PR已被接受,所以它已经包含在Newtonsoft.Json项目中,参考上面链接的CustomDoubleRounding测试以获取示例代码。 - Matt Kemp

10

以后参考,可以通过创建自定义JsonConverter在Json.net中实现相当优雅。

public class DecimalFormatJsonConverter : JsonConverter
{
    private readonly int _numberOfDecimals;

    public DecimalFormatJsonConverter(int numberOfDecimals)
    {
        _numberOfDecimals = numberOfDecimals;
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        var d = (decimal) value;
        var rounded = Math.Round(d, _numberOfDecimals);
        writer.WriteValue((decimal)rounded);
    }

    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 objectType == typeof(decimal);
    }
}

如果你在代码中显式地使用构造函数创建序列化程序,这样做是可行的,但我认为最好使用JsonConverterAttribute来装饰相关属性,这种情况下,类必须具有公共且无参的构造函数。我通过创建一个特定于我想要的格式的子类来解决这个问题。

public class SomePropertyDecimalFormatConverter : DecimalFormatJsonConverter
{
    public SomePropertyDecimalFormatConverter() : base(3)
    {
    }
}

public class Poco 
{
    [JsonConverter(typeof(SomePropertyDecimalFormatConverter))]
    public decimal SomeProperty { get;set; }
}

自定义转换器是从Json.NET文档派生出来的。


你需要做其他什么来激活它吗?我已经添加了类和属性,但它从未进入WriteJson函数。 - NickG
@NickG 不应该有问题。我刚刚用最新的Json.NET和新的控制台应用程序进行了确认。我添加了这里定义的2个转换器类和Poco,然后运行JsonConvert.SerializeObject(new Poco { SomeProperty = 1.23456789m });,结果序列化值为{"SomeProperty":1.235} - htuomola
我误解了这个功能应该如何工作 - 它似乎只在创建JSON时起作用,而不是解析JSON时... 我原本希望它可以将供应商的JSON中的奇怪值转换为更合理的值。例如57.400000000000000001等...然而,我现在已经通过业务逻辑代码完成了这个任务。 - NickG
@NickG,解析应该相对简单,只需将CanRead更改为返回true,并在ReadJson方法中根据需要实现解析即可。不过我还没有测试过。 - htuomola
2
Math.Round(d, _numberOfDecimals) 应该指定 MidpointRounding.AwayFromZero,否则你可能会得到你不期望的舍入结果... 参考这个链接:https://dev59.com/A3NA5IYBdhLWcg3wZ89S - Rosdi Kasim

9

我刚刚遇到了和您一样的问题,因为有些数字被序列化为1.00,而有些则被序列化为1.0000。

我的解决方法是创建一个JsonTextWriter,可以将值四舍五入到小数点后四位。每个小数点都将被四舍五入到四个小数点:1.0变成1.0000,1.0000000也变成1.0000。

private class JsonTextWriterOptimized : JsonTextWriter
{
    public JsonTextWriterOptimized(TextWriter textWriter)
        : base(textWriter)
    {
    }
    public override void WriteValue(decimal value)
    {
        // we really really really want the value to be serialized as "0.0000" not "0.00" or "0.0000"!
        value = Math.Round(value, 4);
        // divide first to force the appearance of 4 decimals
        value = Math.Round((((value+0.00001M)/10000)*10000)-0.00001M, 4); 
        base.WriteValue(value);
    }
}

使用自己的编写器而不是标准编写器:

var jsonSerializer = Newtonsoft.Json.JsonSerializer.Create();
var sb = new StringBuilder(256);
var sw = new StringWriter(sb, CultureInfo.InvariantCulture);
using (var jsonWriter = new JsonTextWriterOptimized(sw))
{
    jsonWriter.Formatting = Formatting.None;
    jsonSerializer.Serialize(jsonWriter, instance);
}

我测试了这个解决方案,它也有效。我不得不将 var jsonSerializer = Newtonsoft.Json.JsonSerializer.Create(); 更改为 var jsonSerializer = Newtonsoft.Json.JsonSerializer.Create(new JsonSerializerSettings()); 才能使其编译通过。可能是因为我使用的是 .Net Framework 4.5? - Halvard

4
在第一种情况下,000 不会造成任何影响,值仍然保持不变,并将被反序列化为完全相同的值。
在第二种情况下,JavascriptSerializer 无法帮助您。JavacriptSerializer 不应更改数据,因为它将其序列化为众所周知的格式,它不提供成员级别的数据转换(但它提供自定义对象转换器)。您想要的是转换 + 序列化,这是一个两阶段任务。
两个建议:
1)使用DataContractJsonSerializer:添加另一个属性来舍入该值:
public class Money
{
    public string Currency { get; set; }

    [IgnoreDataMember]
    public decimal Amount { get; set; }

    [DataMember(Name = "Amount")]
    public decimal RoundedAmount { get{ return Math.Round(Amount, 2); } }
}

2) 克隆对象并将值四舍五入:

public class Money 
{
    public string Currency { get; set; }

    public decimal Amount { get; set; }

    public Money CloneRounding() {
       var obj = (Money)this.MemberwiseClone();
       obj.Amount = Math.Round(obj.Amount, 2);
       return obj;
    }
}

var roundMoney = money.CloneRounding();

我猜json.net也不能做到这一点,但我并不百分百确定。

在我的特定情况下,“000”会造成伤害,因为接收方(我无法控制)在超过两个小数位时会抛出验证错误。除此之外,我一定会考虑你的解决方案。 - Halvard
1
谢谢你的回答 :) 尽管我没有检查克隆建议(我选择了DataContractJsonSerializer建议,并且它运行良好),但我将其设置为正确。 - Halvard

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接