Serializing a decimal to JSON, how to round off?
Asked Answered
P

4

36

I have a class

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

and would like to serialize it to JSON. If I use the JavaScriptSerializer I get

{"Currency":"USD","Amount":100.31000}

Because of the API I have to conform to needs JSON amounts with maximum two decimal places, I feel it should be possible to somehow alter the way the JavaScriptSerializer serializes a decimal field, but I can't find out how. There is the SimpleTypeResolver you can pass in the constructor, but it only work on types as far as I can understand. The JavaScriptConverter, which you can add through RegisterConverters(...) seems to be made for Dictionary.

I would like to get

{"Currency":"USD","Amount":100.31}

after I serialize. Also, changing to double is out of the question. And I probably need to do some rounding (100.311 should become 100.31).

Does anyone know how to do this? Is there perhaps an alternative to the JavaScriptSerializer that lets you control the serializing in more detail?

Pedicure answered 5/9, 2012 at 13:53 Comment(3)
Do you only want to round the amount when serializing? You could possibly add another property to the class and serialize that instead of the Amount property and mark the original Amount property to not be serialized.Ummersen
@MarkSherretta Yes, I only want to round when serializing to JSON. Can I do that without turning it into a string ("Amount":"100.31")? A rounded double field for serializing?Pedicure
Preferably I would like the change to be in one place only. In my case it is OK to do it for all decimals being serialized (not just in this object).Pedicure
D
4

In the first case the 000 does no harm, the value still is the same and will be deserialized to the exact same value.

In the second case the JavascriptSerializer will not help you. The JavacriptSerializer is not supposed to change the data, since it serializes it to a well-known format it does not provide data conversion at member level (but it provides custom Object converters). What you want is a conversion + serialization, this is a two-phases task.

Two suggestions:

1) Use DataContractJsonSerializer: add another property that rounds the value:

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) Clone the object rounding the values:

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();

I guess json.net cannot do this either, but I'm not 100% sure.

Demb answered 5/9, 2012 at 14:36 Comment(2)
In my specific case 000 does harm since the receiving part (over which I don't have any control) throws a validation error on more than two decimals. That aside, I will certainly consider your solution.Pedicure
Thanks for your answer :) I am setting it to correct even though I have not checked the cloning-suggestion (I went for the DataContractJsonSerializer-suggestion, and it works fine).Pedicure
P
24

I wasn't completely satisfied with all of the techniques thus far to achieve this. JsonConverterAttribute seemed the most promising, but I couldn't live with hard-coded parameters and proliferation of converter classes for every combination of options.

So, I submitted a PR that adds the ability to pass various arguments to JsonConverter and JsonProperty. It's been accepted upstream and I expect will be in the next release (whatever's next after 6.0.5)

You can then do it like this:

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; }
}

Refer to the CustomDoubleRounding() test for an example.

Paraph answered 23/9, 2014 at 15:26 Comment(5)
I can't make this work in version 8.. Is there a complete example of whats required to make this work? The code runs OK but doesn't round anything.Diamante
Oh, just realised it doesn't work when deserializing - only when creating serializing to new JSON :(Diamante
Try changing CanRead to return true, and then implement a proper ReadJson. There's no reason I can think of that this cannot be easily made bidirectional.Paraph
Thanks - I'll give that a go. I wasn't sure what caused it to use the ReadJSON function but now that you've said that - I guess I should have spotted it :)Diamante
This is the best answer I've found, the PR was accepted so this is in Newtonsoft.Json project, refer to the CustomDoubleRounding test linked above for example codeSukkah
K
10

For future reference, this can be achieved in Json.net pretty elegantly by creating a custom JsonConverter

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);
    }
}

If you're creating serializers in code using constructor explicitly, this will work fine but I think it's nicer to decorate the relevant properties with JsonConverterAttribute, in which case the class must have a public, parameterless constructor. I solved this by creating a subclass which is specific to the format I want.

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

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

The custom converter has been derived from Json.NET documentation.

Kazan answered 12/6, 2014 at 8:51 Comment(5)
Is there anything else you have to do to activate this? I've added the class and attribute, but it never enters the WriteJson function.Diamante
@Diamante it shouldn't. I just confirmed with new console app and latest Json.NET. I added the 2 converter classes and the Poco as defined here and JsonConvert.SerializeObject(new Poco { SomeProperty = 1.23456789m }); results in serialized value of {"SomeProperty":1.235}Kazan
I was misunderstanding how this should work - it only seems to work when creating JSON - not when parsing it... I was hoping it would convert the weird values in the supplier's JSON into more sensible ones. Eg 57.400000000000000001 etc... However I've now done this in biz logic code.Diamante
@Diamante parsing should be relatively simple to support too, changing CanRead to return true and implementing the parsing as necessary in ReadJson method. I haven't tested it though.Kazan
Math.Round(d, _numberOfDecimals) should specify MidpointRounding.AwayFromZero otherwise you might get rounding you dont expect... refer to this: #978296Kept
G
9

I just went through the same trouble as I had some decimals being serialized with 1.00 and some with 1.0000. This is my change:

Create a JsonTextWriter that can round the value to 4 decimals. Every decimal will then be rounded to 4 decimals: 1.0 becomes 1.0000 and 1.0000000 becomes also 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);
    }
}

Use your own writer instead of the standard one:

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);
}
Grijalva answered 6/2, 2013 at 12:47 Comment(1)
I tested this solution and it works too. I had to change var jsonSerializer = Newtonsoft.Json.JsonSerializer.Create(); to var jsonSerializer = Newtonsoft.Json.JsonSerializer.Create(new JsonSerializerSettings()); to get it to compile. Possibly it is because I am on .Net framework 4.5?Pedicure
D
4

In the first case the 000 does no harm, the value still is the same and will be deserialized to the exact same value.

In the second case the JavascriptSerializer will not help you. The JavacriptSerializer is not supposed to change the data, since it serializes it to a well-known format it does not provide data conversion at member level (but it provides custom Object converters). What you want is a conversion + serialization, this is a two-phases task.

Two suggestions:

1) Use DataContractJsonSerializer: add another property that rounds the value:

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) Clone the object rounding the values:

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();

I guess json.net cannot do this either, but I'm not 100% sure.

Demb answered 5/9, 2012 at 14:36 Comment(2)
In my specific case 000 does harm since the receiving part (over which I don't have any control) throws a validation error on more than two decimals. That aside, I will certainly consider your solution.Pedicure
Thanks for your answer :) I am setting it to correct even though I have not checked the cloning-suggestion (I went for the DataContractJsonSerializer-suggestion, and it works fine).Pedicure

© 2022 - 2024 — McMap. All rights reserved.