34

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?

Halvard
  • 3,891
  • 6
  • 39
  • 51
  • 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. – Mark Sherretta Sep 05 '12 at 14:01
  • @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? – Halvard Sep 05 '12 at 14:04
  • 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). – Halvard Sep 05 '12 at 14:11

4 Answers4

23

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.

BrandonLWhite
  • 1,866
  • 1
  • 23
  • 26
  • 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. – NickG Feb 05 '16 at 11:26
  • Oh, just realised it doesn't work when deserializing - only when creating serializing to new JSON :( – NickG Feb 05 '16 at 11:31
  • 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. – BrandonLWhite Feb 05 '16 at 13:56
  • 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 :) – NickG Feb 05 '16 at 16:49
  • 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 code – Matt Kemp Sep 30 '18 at 21:36
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.

htuomola
  • 715
  • 4
  • 9
  • 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. – NickG Feb 05 '16 at 11:14
  • @NickG 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}` – htuomola Feb 08 '16 at 06:45
  • 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. – NickG Feb 08 '16 at 11:45
  • @NickG 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. – htuomola Feb 09 '16 at 14:04
  • 2
    `Math.Round(d, _numberOfDecimals)` should specify `MidpointRounding.AwayFromZero` otherwise you might get rounding you dont expect... refer to this: http://stackoverflow.com/questions/977796/why-does-math-round2-5-return-2-instead-of-3 – Rosdi Kasim May 09 '17 at 03:48
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);
}
Corneliu
  • 818
  • 9
  • 15
  • 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? – Halvard Feb 18 '13 at 12:42
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.

Marcelo De Zen
  • 9,439
  • 3
  • 37
  • 50
  • 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. – Halvard Sep 05 '12 at 14:40
  • 1
    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). – Halvard Sep 06 '12 at 08:28