Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Handling decimal values in Newtonsoft.Json

Edit: It's been almost 5 years and I don't think this is the way to go. The client should post the data in the correct numerical format. With current frameworks like React or Angular, or with a proper architecture and error handling & validation, i think this is almost a non-problem.

But if anyone wishes to flex their Json.NET muscles, feel free to check the answers.


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?

like image 614
Ricardo Pieper Avatar asked Jun 05 '14 03:06

Ricardo Pieper


People also ask

Can you use decimals in JSON?

JSON does not have distinct types for integers and floating-point values. Therefore, the presence or absence of a decimal point is not enough to distinguish between integers and non-integers. For example, 1 and 1.0 are two ways to represent the same value in JSON.

Why would you use a string in JSON to represent a decimal number?

The main reason to transfer numeric values in JSON as strings is to eliminate any loss of precision or ambiguity in transfer.

What is Newtonsoft JSON formatting indented?

Use Namespace Newtonsoft.Json.Formatting Newtonsoft.Json.Formatting provides formatting options to Format the Json. None − No special formatting is applied. This is the default. Indented − Causes child objects to be indented according to the Newtonsoft.

What does Jsonconvert Deserializeobject do?

Deserializes the JSON to the specified . NET type. Deserializes the JSON to the specified . NET type using a collection of JsonConverter.


1 Answers

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() }
};
like image 71
Brian Rogers Avatar answered Oct 23 '22 18:10

Brian Rogers