9

I have some .NET code that deserializes JSON objects created by a webservice running a dynamic language. Because the source is dynamic, it sometimes serializes integral values in float format (e. g. 2 gets serialized to "2.0").

With Json.NET 4.0.4, this worked seamlessly (seems like rounding was applied when deserializing). With the upgrade to Json.NET 4.5, though, deserializing 2.0 now throws a FormatException. Here's the code:

// works as expected in both versions
var s = "2";
Console.WriteLine(JsonConvert.DeserializeObject<int>(s));

// throws FormatException in 4.5 only
var s = "2.0";
Console.WriteLine(JsonConvert.DeserializeObject<int>(s));

// throws FormatException in 4.5, rounds to 3 in 4.0.4
var s = "2.6";
Console.WriteLine(JsonConvert.DeserializeObject<int>(s));

Is there any easy way to restore the original behavior? The ideal behavior would be to deserialize only numbers with integral values, but in any format (e. g. 2.0, 1e10, but not 2.5), but I'd settle for the 4.0.4 behavior.

Microsoft DN
  • 9,706
  • 10
  • 51
  • 71
ChaseMedallion
  • 20,860
  • 17
  • 88
  • 152

1 Answers1

6

You can do this by making a custom JsonConverter which will handle rounding (or discarding) the decimal values. It might look something like this:

class CustomIntConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return (objectType == typeof(int));
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        JValue jsonValue = serializer.Deserialize<JValue>(reader);

        if (jsonValue.Type == JTokenType.Float)
        {
            return (int)Math.Round(jsonValue.Value<double>());
        }
        else if (jsonValue.Type == JTokenType.Integer)
        {
            return jsonValue.Value<int>();
        }

        throw new FormatException();
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}

You can then use the custom converter like this:

JsonSerializerSettings settings = new JsonSerializerSettings 
{
    Converters = new List<JsonConverter> { new CustomIntConverter() } 
};

string json = @"[2.6, 0, 4.1, 5, -3, -2.2]";

List<int> list = JsonConvert.DeserializeObject<List<int>>(json, settings);

foreach (int val in list)
{
    Console.WriteLine(val);
}

The output of the above would be this:

3
0
4
5
-3
-2

If you would rather the converter ignore decimal values rather than round them, replace the following line of code

        return (int)Math.Round(jsonValue.Value<double>());

with this:

        return (existingValue ?? default(int));

After making that change, the output of the test code above would then look like this:

0
0
0
5
-3
0
Brian Rogers
  • 125,747
  • 31
  • 299
  • 300
  • Might need to consider adding `objectType == typeof(object)` in `CanConvert`, if the target class "doesn't specify" the actual type e.g. `class MyObject { public object Id; }` – drzaus Feb 26 '15 at 18:05
  • 1
    @drzaus Maybe, but be careful. If you do that and you have any other properties of type `object` that are *not* numbers, this converter will try to handle them, which may not be what you are expecting. You'd have to add code to handle that case. – Brian Rogers Feb 26 '15 at 18:50
  • Ah right, I was thinking of some [other answers](http://stackoverflow.com/a/28748973/1037948) that handle it by checking the `JTokenType` and if it's not an expected type, rather than throwing a `FormatException` just return `serializer.Deserialize(reader)`, which seems to delegate it on to whatever else should appropriately handle it. It's worked so far in my tests with nested, complex types. – drzaus Feb 26 '15 at 19:40
  • Thanks for this. In our case Int32 values were being deserialized as Int64 causing binding problems for OData's `Delta`. Setting CanWrite to return false and checking on `typeof(Int64)` has worked a treat. – Jamie Dixon Jun 08 '15 at 10:54