Ricardo Pieper Ricardo Pieper - 5 months ago 32
JSON Question

Handling decimal values in Newtonsoft.Json

I have a MVC application and I handle some JSON in it. That's simple. I have this simple piece of code in my ModelBinder:

return JsonConvert.DeserializeObject(jsonString, bindingContext.ModelType, new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Ignore,
MissingMemberHandling = MissingMemberHandling.Ignore,
Formatting = Formatting.None,
DateFormatHandling = DateFormatHandling.IsoDateFormat,
FloatParseHandling = FloatParseHandling.Decimal
});


And it works flawlessly.

Well, sort of.

Let's say I have this class:

public class MyClass
{
public decimal MyProp { get; set; }
}


If I try to deserialize this json:

"{\"MyProp\": 9888.77}"


Of course it works, since
9888.77
is a Javascript float value. I think.

But I have a masked input for money in my page that makes the JSON look like this (sorry about my english):

"{\"MyProp\": \"9.888,77\" }"


AAAND, it fails. It says that it
Could not convert string to decimal
.

Ok, that's fair. It is not a JS float, but
Convert.ToDecimal("9.888,77")
works the way I want.

I've read some tutorials on the internet about custom deserializers, but its inviable for me to define a custom deserializer for every single class I have in my application.

What I want is to simple redefine the way JSON.Net converts a string to a decimal property, in any class i'll ever want to deserialize to. I want to inject the
Convert.ToDecimal
function in the process of converting decimals, when the current converter doesn't work.

Is there a way I could do it?

I thought there was a way to do it, so I changed my code a little bit.

JsonSerializer serializer = new JsonSerializer
{
NullValueHandling = NullValueHandling.Ignore,
MissingMemberHandling = MissingMemberHandling.Ignore,
Formatting = Formatting.None,
DateFormatHandling = DateFormatHandling.IsoDateFormat,
FloatParseHandling = FloatParseHandling.Decimal,
};



return serializer.Deserialize(new DecimalReader(jsonStr), bindingContext.ModelType);


And created this class:

public class DecimalReader : JsonTextReader
{
public DecimalReader(string s)
: base(new StringReader(s))
{
}

public override decimal? ReadAsDecimal()
{
try
{
return base.ReadAsDecimal();
}
catch (Exception)
{
if (this.TokenType == JsonToken.String)
{
decimal value = 0;

bool convertible = Decimal.TryParse(this.Value.ToString(), out value);

if (convertible)
{
return new Nullable<decimal>(value);
}
else { throw; }
}
else
{
throw;
}
}
}
}


But it is very ugly: it executes what I want only when it crashes, and depends on
base.ReadAsDecimal()
crashing
. It couldn't be more ugly.

And doesn't work :
Error converting value "1.231,23" to type 'System.Nullable1[System.Decimal]'. Path 'MyProp', line X, position Y.


The value itself is being converted, but perhaps for some reason it still tries to put the string "1.231,23" into a decimal.

So, is there a way to do it properly?

Answer

You can handle both formats (the JSON number representation and the masked string format) using a custom JsonConverter class like this.

class DecimalConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return (objectType == typeof(decimal) || objectType == typeof(decimal?));
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        JToken token = JToken.Load(reader);
        if (token.Type == JTokenType.Float || token.Type == JTokenType.Integer)
        {
            return token.ToObject<decimal>();
        }
        if (token.Type == JTokenType.String)
        {
            // customize this to suit your needs
            return Decimal.Parse(token.ToString(), 
                   System.Globalization.CultureInfo.GetCultureInfo("es-ES"));
        }
        if (token.Type == JTokenType.Null && objectType == typeof(decimal?))
        {
            return null;
        }
        throw new JsonSerializationException("Unexpected token type: " + 
                                              token.Type.ToString());
    }

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

To plug this into your binder, just add an instance of the converter to the Converters list in the JsonSerializerSettings object:

JsonSerializerSettings settings = new JsonSerializerSettings
{
    NullValueHandling = NullValueHandling.Ignore,
    MissingMemberHandling = MissingMemberHandling.Ignore,
    Formatting = Formatting.None,
    DateFormatHandling = DateFormatHandling.IsoDateFormat,
    Converters = new List<JsonConverter> { new DecimalConverter() }
};
Comments