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?


Solution 1:

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

Solution 2:

Thanx a lot! I was looking for a solution to make decimals always serialize in a similar manner and this post sent me in the right direction. This is my code:

    internal 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)
        {
            throw new NotImplementedException();
        }

        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            Decimal? d = default(Decimal?);
            if (value != null)
            {
                d = value as Decimal?;
                if (d.HasValue) // If value was a decimal?, then this is possible
                {
                    d = new Decimal?(new Decimal(Decimal.ToDouble(d.Value))); // The ToDouble-conversion removes all unnessecary precision
                }
            }
            JToken.FromObject(d).WriteTo(writer);
        }
    }

Solution 3:

As an extension to Kwaazaar's answer, I have added the reverse way to the converter as well (In his example it throws a NotImplementedException.

namespace Something.Converter
{
    using System;

    using Newtonsoft.Json;
    using Newtonsoft.Json.Linq;

    /// <inheritdoc cref="JsonConverter"/>
    /// <summary>
    /// Converts an object to and from JSON.
    /// </summary>
    /// <seealso cref="JsonConverter"/>
    public class DecimalConverter : JsonConverter
    {
        /// <summary>
        /// Gets a new instance of the <see cref="DecimalConverter"/>.
        /// </summary>
        public static readonly DecimalConverter Instance = new DecimalConverter();

        /// <inheritdoc cref="JsonConverter"/>
        /// <summary>
        /// Determines whether this instance can convert the specified object type.
        /// </summary>
        /// <param name="objectType">Type of the object.</param>
        /// <returns>
        ///     <c>true</c> if this instance can convert the specified object type; otherwise, <c>false</c>.
        /// </returns>
        /// <seealso cref="JsonConverter"/>
        public override bool CanConvert(Type objectType)
        {
            return objectType == typeof(decimal) || objectType == typeof(decimal?);
        }

        /// <inheritdoc cref="JsonConverter"/>
        /// <summary>
        /// Reads the JSON representation of the object.
        /// </summary>
        /// <param name="reader">The <see cref="JsonReader"/> to read from.</param>
        /// <param name="objectType">Type of the object.</param>
        /// <param name="existingValue">The existing value of object being read.</param>
        /// <param name="serializer">The calling serializer.</param>
        /// <returns>The object value.</returns>
        /// <seealso cref="JsonConverter"/>
        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            if (!(reader.Value is string value))
            {
                if (objectType == typeof(decimal?))
                {
                    return null;
                }

                return default(decimal);
            }

            // ReSharper disable once StyleCop.SA1126
            if (decimal.TryParse(value, out var result))
            {
                // ReSharper disable once StyleCop.SA1126
                return result;
            }

            if (objectType == typeof(decimal?))
            {
                return null;
            }

            return default(decimal);
        }

        /// <inheritdoc cref="JsonConverter"/>
        /// <summary>
        /// Writes the JSON representation of the object.
        /// </summary>
        /// <param name="writer">The <see cref="JsonWriter"/> to write to.</param>
        /// <param name="value">The value.</param>
        /// <param name="serializer">The calling serializer.</param>
        /// <seealso cref="JsonConverter"/>
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            var d = default(decimal?);

            if (value != null)
            {
                d = value as decimal?;
                if (d.HasValue)
                {
                    d = new decimal(decimal.ToDouble(d.Value));
                }
            }

            JToken.FromObject(d ?? 0).WriteTo(writer);
        }
    }
}

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
{
    // Some other settings.
    Converters = new List<JsonConverter> { new DecimalConverter() }
};

or

JsonSerializerSettings settings = new JsonSerializerSettings
{
    // Some other settings.
    Converters = new List<JsonConverter> { DecimalConverter.Instance }
};