orleans: Bug in Json.Net: System.Int32 is deserialized as System.Int64

My state object produces errors during serialization in the Boolean object. Do I need to update to 1.1.1?

This is the object:

[JsonObject(MemberSerialization.OptIn)]
public class SiteState : global::Orleans.GrainState
{
    [JsonProperty]
    public AsSite Model { get; set; }

    //defaults
    [JsonProperty]
    public int Threshold { get; set; }

    [JsonProperty]
    public bool DisableAlarms { get; set; }

}

and this is the error:

Data #1 Key=Model Value=Mesh.Ratatouille.Model.Site.AsSite Type=Mesh.Ratatouille.Model.Site.AsSite Data #2 Key=Threshold Value=0 Type=System.Int64 Data #3 Key=DisableAlarms Value=False Type=System.Boolean at Orleans.Storage.AzureTableStorage.ConvertFromStorageFormat(GrainState grainState, GrainStateEntity entity) at Orleans.Storage.AzureTableStorage.<ReadStateAsync>d__0.MoveNext() — End of stack trace from previous location where exception was thrown — at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at System.Runtime.CompilerServices.TaskAwaiter.GetResult() at Orleans.Runtime.Scheduler.SchedulerExtensions.<>c__DisplayClassa.<<QueueTask>b__8>d__c.MoveNext() — End of stack trace from previous location where exception was thrown — at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at System.Runtime.CompilerServices.TaskAwaiter.GetResult() at Orleans.Runtime.Catalog.<SetupActivationState>d__35.MoveNext() — End of stack trace from previous location where exception was thrown — at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at System.Runtime.CompilerServices.TaskAwaiter.GetResult() at Orleans.Runtime.Catalog.<InitActivation>d__2b.MoveNext() Exc level 1: System.ArgumentException: Object of type ‘System.Int64’ cannot be converted to type ‘System.Int32’. at System.RuntimeType.TryChangeType(Object value, Binder binder, CultureInfo culture, Boolean needsSpecialCast) at System.RuntimeType.CheckValue(Object value, Binder binder, CultureInfo culture, BindingFlags invokeAttr) at System.Reflection.MethodBase.CheckArguments(Object[] parameters, Binder binder, BindingFlags invokeAttr, CultureInfo culture, Signature sig) at System.Reflection.RuntimeMethodInfo.InvokeArgumentsCheck(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture) at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture) at System.Reflection.RuntimePropertyInfo.SetValue(Object obj, Object value, Object[] index) at Orleans.GrainState.SetAll(IDictionary`2 values) at Orleans.Storage.AzureTableStorage.ConvertFromStorageFormat(GrainState grainState, GrainStateEntity entity)

About this issue

  • Original URL
  • State: closed
  • Created 8 years ago
  • Comments: 17 (9 by maintainers)

Most upvoted comments

@gabikliot

There are additional gotcha in this area that I discovered when deserializing using Json.NET with weak typing (i.e. when Json.NET doesn’t have specific type information during deserialization):

  1. As you’ve discovered, all numbers are deserialized as long. Well, almost all numbers. If you deserialize a UInt64 with a value larger than Int64.MaxValue, it will deserialize into a BigInteger instead of a UInt64.
  2. You’ve also discovered that Guid will be deserialized as String.
  3. System.Single is deserialized into System.Double unless specified otherwise by JsonSerializerSettings.FloatParseHandling.
  4. Enums are serialized as numbers, so they will be deserialized as Int64, unless StringEnumConverter was used for serialization, then it will be deserialized as a String. In any case, conversion should be tolerant to both.

Due to all this, I’ve written my own conversion code to fix any loss of type information when converting to JSON, included below. It’s not perfect, and it evolves as I discover new corner cases, but it addresses some of the oddities you might encounter.

public static class JsonHelper
{
    private static readonly Type[] _specialNumericTypes = { typeof(ulong), typeof(uint), typeof(ushort), typeof(sbyte) };

    /// <summary>
    /// Converts values that were deserialized from JSON with weak typing (e.g. into <see cref="object"/>) back into
    /// their strong type, according to the specified target type.
    /// </summary>
    /// <param name="value">The value to convert.</param>
    /// <param name="targetType">The type the value should be converted into.</param>
    /// <returns>The converted value.</returns>
    public static object ConvertWeaklyTypedValue(object value, Type targetType)
    {
        if (targetType == null)
            throw new ArgumentNullException(nameof(targetType));

        if (value == null)
            return null;

        if (targetType.IsInstanceOfType(value))
            return value;

        var paramType = Nullable.GetUnderlyingType(targetType) ?? targetType;

        if (paramType.IsEnum)
        {
            if (value is string)
                return Enum.Parse(paramType, (string)value);
            else
                return Enum.ToObject(paramType, value);
        }

        if (paramType == typeof(Guid))
        {
            return Guid.Parse((string)value);
        }

        if (_specialNumericTypes.Contains(paramType))
        {
            if (value is BigInteger)
                return (ulong)(BigInteger)value;
            else
                return Convert.ChangeType(value, paramType);
        }

        if (value is long)
        {
            return Convert.ChangeType(value, paramType);
        }

        throw new ArgumentException($"Cannot convert a value of type {value.GetType()} to {targetType}.");
    }
}