1

Closed

InvalidCastException when deserializing custom IConvertible

description

Since upgrading JSon.NET from 4.5 to 5.0.5 my custom value type (a mathematical Ratio<T> class that implements IConvertible) no longer deserializes successfully. Instead I get an InvalidCastException.

The problem occurs because for example a Ratio<int> value is serialized as single number string if the ratio represents a whole number, whereas it is serialised as a fraction (for example 1/2) otherwise. When deserializing an integer source value into a Ratio<int> target value, the conversion procedure recognizes both the source value (Int32 type) and the target value (Ratio<int> type) as IConvertible and calls Convert.ChangeType(intValue, typeof(Ratio<int>), culture), causing an InvalidCastException because the System.Int32 class doesn't know how to convert to Ratio<int> values.

For this reason Convert.ChangeType should not be relied on when deserialising values into IConvertible types that are non-system types.
Closed May 14, 2013 at 11:22 PM by JamesNK
Thanks. Fixed

comments

JamesNK wrote May 14, 2013 at 7:13 AM

Which version were you using? Can you attach a demo of what used to work?

ggeurts wrote May 14, 2013 at 10:32 AM

My unit tests pass when using version 4.5.11, but fail under 5.0.5. I will try to get some sample code to illustrate the issue.

ggeurts wrote May 14, 2013 at 12:05 PM

namespace JsonConvertible
{
    using System;
    using System.Globalization;
    using System.Runtime.Serialization;
    using Newtonsoft.Json;

    class Program
    {
        static void Main(string[] args)
        {
            var ratio = new Ratio(2, 1);
            var json = JsonConvert.SerializeObject(ratio);

            // Throws exception in Newtonsoft.Json 5.0.5
            var ratio2 = JsonConvert.DeserializeObject<Ratio>(json);
        }
    }

    public struct Ratio: IConvertible, IFormattable, ISerializable
    {
        private readonly int _numerator;
        private readonly int _denominator;

        public Ratio(int numerator, int denominator)
        {
            _numerator = numerator;
            _denominator = denominator;
        }

        #region Properties

        public int Numerator
        {
            get { return _numerator; }
        }

        public int Denominator
        {
            get { return _denominator; }
        }

        public bool IsNan
        {
            get { return _denominator == 0; }
        }

        #endregion

        #region Serialization operations

        public Ratio(SerializationInfo info, StreamingContext context)
        {
            _numerator = info.GetInt32("n");
            _denominator = info.GetInt32("d");
        }

        public void GetObjectData(SerializationInfo info, StreamingContext context)
        {
            info.AddValue("n", _numerator);
            info.AddValue("d", _denominator);
        }

        #endregion

        #region IConvertible Members

        public TypeCode GetTypeCode()
        {
            return TypeCode.Object;
        }

        public bool ToBoolean(IFormatProvider provider)
        {
            return this._numerator == 0;
        }

        public byte ToByte(IFormatProvider provider)
        {
            return (byte)(this._numerator / this._denominator);
        }

        public char ToChar(IFormatProvider provider)
        {
            return Convert.ToChar(this._numerator / this._denominator);
        }

        public DateTime ToDateTime(IFormatProvider provider)
        {
            return Convert.ToDateTime(this._numerator / this._denominator);
        }

        public decimal ToDecimal(IFormatProvider provider)
        {
            return (decimal)this._numerator / this._denominator;
        }

        public double ToDouble(IFormatProvider provider)
        {
            return this._denominator == 0
                ? double.NaN
                : (double)this._numerator / this._denominator;
        }

        public short ToInt16(IFormatProvider provider)
        {
            return (short)(this._numerator / this._denominator);
        }

        public int ToInt32(IFormatProvider provider)
        {
            return this._numerator / this._denominator;
        }

        public long ToInt64(IFormatProvider provider)
        {
            return this._numerator / this._denominator;
        }

        public sbyte ToSByte(IFormatProvider provider)
        {
            return (sbyte)(this._numerator / this._denominator);
        }

        public float ToSingle(IFormatProvider provider)
        {
            return this._denominator == 0
                ? float.NaN
                : (float)this._numerator / this._denominator;
        }

        public string ToString(IFormatProvider provider)
        {
            return this._denominator == 1
                ? this._numerator.ToString(provider)
                : this._numerator.ToString(provider) + "/" + this._denominator.ToString(provider);
        }

        public object ToType(Type conversionType, IFormatProvider provider)
        {
            return Convert.ChangeType(ToDouble(provider), conversionType, provider);
        }

        public ushort ToUInt16(IFormatProvider provider)
        {
            return (ushort)(this._numerator / this._denominator);
        }

        public uint ToUInt32(IFormatProvider provider)
        {
            return (uint)(this._numerator / this._denominator);
        }

        public ulong ToUInt64(IFormatProvider provider)
        {
            return (ulong)(this._numerator / this._denominator);
        }

        #endregion

        #region String operations

        public override string ToString()
        {
            return ToString(CultureInfo.InvariantCulture);
        }

        public string ToString(string format, IFormatProvider formatProvider)
        {
            return ToString(CultureInfo.InvariantCulture);
        }

        public static Ratio Parse(string input)
        {
            return Parse(input, CultureInfo.InvariantCulture);
        }

        public static Ratio Parse(string input, IFormatProvider formatProvider)
        {
            Ratio result;
            if (!TryParse(input, formatProvider, out result))
            {
                throw new FormatException(
                    string.Format(
                        CultureInfo.InvariantCulture, 
                        "Text '{0}' is invalid text representation of ratio", 
                        input));
            }
            return result;
        }

        public static bool TryParse(string input, out Ratio result)
        {
            return TryParse(input, CultureInfo.InvariantCulture, out result);
        }

        public static bool TryParse(string input, IFormatProvider formatProvider, out Ratio result)
        {
            if (input != null)
            {
                var fractionIndex = input.IndexOf('/');

                int numerator;
                if (fractionIndex < 0)
                {
                    if (int.TryParse(input, NumberStyles.Integer, formatProvider, out numerator))
                    {
                        result = new Ratio(numerator, 1);
                        return true;
                    }
                }
                else
                {
                    int denominator;
                    if (int.TryParse(input.Substring(0, fractionIndex), NumberStyles.Integer, formatProvider, out numerator) &&
                        int.TryParse(input.Substring(fractionIndex + 1), NumberStyles.Integer, formatProvider, out denominator))
                    {
                        result = new Ratio(numerator, denominator);
                        return true;
                    }
                }
            }

            result = default(Ratio);
            return false;
        }

        #endregion
    }

}