AFgone
AFgone

Reputation: 1250

JSON.NET Unable to deserialize ulong flag type enum

I have a flag type of enum and I would like it to be serialized not as string but as number. There is no problem about serializing but json.net unable to deserialize it.

public class ForceNumericFlagEnumConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        if (!(Nullable.GetUnderlyingType(objectType) ?? objectType).IsEnum)
            return false;
        return HasFlagsAttribute(objectType);
    }

    public override bool CanRead { get { return false; } }

    public override bool CanWrite { get { return false; } }

    static bool HasFlagsAttribute(Type objectType) 
    { 
        return Attribute.IsDefined(Nullable.GetUnderlyingType(objectType) ?? objectType, typeof(System.FlagsAttribute));
    }

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

public class Program
{
    public static void Main()
    {
        try
        {
            Test(UserCan.ChangeConf | UserCan.ForceOther);
            Test(UserCan.DoNothing);
            Test(UserCan.ForceOther);
            Test(UserCan.DoEverything);
        }
        catch (Exception ex)
        {
            Console.WriteLine("Uncaught exception:");
            Console.WriteLine(ex);
            throw;
        }
    }

    static void Test(UserCan ability)
    {
        JsonConvert.DefaultSettings = () => GetGlobalJsonSettings();

        var settings = GetNEWJsonSettings();

        string jsonAbility = JsonConvert.SerializeObject(ability, settings);
        Console.WriteLine("\nJSON serialization for {0} \"{1}\":", ability.GetType(), ability);
        Console.WriteLine(jsonAbility);

        ulong val;
        Assert.IsTrue(ulong.TryParse(jsonAbility, out val));

        var result1 = JsonConvert.DeserializeObject<UserCan?>(jsonAbility, settings);
        var result2 = JsonConvert.DeserializeObject<UserCan>(jsonAbility, settings);

        Assert.IsTrue(result1 == ability);
        Assert.IsTrue(result2 == ability);

        Assert.AreEqual("\"someValue\"", JsonConvert.SerializeObject(NonFlagEnum.SomeValue, settings));
    }

    public enum NonFlagEnum
    {
        SomeValue,
    }

    [Flags]
    public enum UserCan : ulong
    {
        DoNothing = 0,
        ChangeConf =               1 << 0,
        MonitorOthers =              1 << 1,
        ForceOther =                1 << 2,
        EditRemoveOthers =           1 << 3,

        DoEverything = unchecked((ulong)~0),
    }

    public static JsonSerializerSettings GetGlobalJsonSettings()
    {       
        JsonSerializerSettings settings = new JsonSerializerSettings
        {
            Converters = { new StringEnumConverter{ CamelCaseText = true } },
        };
        return settings;
    }

    public static JsonSerializerSettings GetNEWJsonSettings()
    {
        JsonSerializerSettings settings = new JsonSerializerSettings
        {
            DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate,
            NullValueHandling = NullValueHandling.Ignore,
            ObjectCreationHandling = ObjectCreationHandling.Replace, 
            TypeNameHandling = TypeNameHandling.Auto,
            Converters = { new ForceNumericFlagEnumConverter() },
        };
        return settings;
    }
}

You can see full code to create error here

https://dotnetfiddle.net/y0GnNf

[Newtonsoft.Json.JsonSerializationException: Error converting value 18446744073709551615 to type 'Program+UserCan'. Path '', line 1, position 20.]

Upvotes: 2

Views: 1168

Answers (2)

dbc
dbc

Reputation: 116670

You have encountered a known bug in Json.NET with ulong-type enumerations: Failure to deserialize ulong enum #2301

Serialization of ulong enum values works fine,
but deserialization fails on large values.

As a workaround, if you are not using Json.NET's StringEnumConverter, you can add the following converter that correctly handles very large values for ulong enums:

public class FixedUlongEnumConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        var enumType = Nullable.GetUnderlyingType(objectType) ?? objectType;
        if (!enumType.IsEnum)
            return false;
        return Enum.GetUnderlyingType(enumType) == typeof(ulong);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        var nullableUnderlying = Nullable.GetUnderlyingType(objectType);
        var enumType = nullableUnderlying ?? objectType;
        var isNullable = nullableUnderlying != null;
        switch (reader.MoveToContentAndAssert().TokenType)
        {
            case JsonToken.Null:
                if (!isNullable)
                    throw new JsonSerializationException(string.Format("Null value for {0}", objectType));
                return null;

            case JsonToken.Integer:
                if (reader.ValueType == typeof(System.Numerics.BigInteger))
                {
                    var bigInteger = (System.Numerics.BigInteger)reader.Value;
                    if (bigInteger >= ulong.MinValue && bigInteger <= ulong.MaxValue)
                    {
                        return Enum.ToObject(enumType, checked((ulong)bigInteger));
                    }
                    else
                    {
                        throw new JsonSerializationException(string.Format("Value {0} is too large for enum {1}", bigInteger, enumType));
                    }
                }
                else
                {
                    return Enum.ToObject(enumType, reader.Value);
                }

            default:
                throw new JsonSerializationException(string.Format("Unexpected token {0}", reader.TokenType));          
        }
    }

    public override bool CanWrite { get { return false; } }

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

public static partial class JsonExtensions
{
    public static JsonReader MoveToContentAndAssert(this JsonReader reader)
    {
        if (reader == null)
            throw new ArgumentNullException();
        if (reader.TokenType == JsonToken.None)       // Skip past beginning of stream.
            reader.ReadAndAssert();
        while (reader.TokenType == JsonToken.Comment) // Skip past comments.
            reader.ReadAndAssert();
        return reader;
    }

    public static JsonReader ReadAndAssert(this JsonReader reader)
    {
        if (reader == null)
            throw new ArgumentNullException();
        if (!reader.Read())
            throw new JsonReaderException("Unexpected end of JSON stream.");
        return reader;
    }
}

Demo fiddle #1 here.

And if you are using Json.NET's StringEnumConverter but might sometimes get numeric values for enums anyway, replace it with this fixed version:

public class FixedUlongStringEnumConverter : StringEnumConverter
{
    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (reader.MoveToContentAndAssert().TokenType == JsonToken.Integer && reader.ValueType == typeof(System.Numerics.BigInteger))
        {
            // Todo: throw an exception if !this.AllowIntegerValues
            // https://www.newtonsoft.com/json/help/html/P_Newtonsoft_Json_Converters_StringEnumConverter_AllowIntegerValues.htm
            var enumType = Nullable.GetUnderlyingType(objectType) ?? objectType;
            if (Enum.GetUnderlyingType(enumType) == typeof(ulong))
            {
                var bigInteger = (System.Numerics.BigInteger)reader.Value;
                if (bigInteger >= ulong.MinValue && bigInteger <= ulong.MaxValue)
                {
                    return Enum.ToObject(enumType, checked((ulong)bigInteger));
                }
            }
        }
        return base.ReadJson(reader, objectType, existingValue, serializer);
    }
}

public static partial class JsonExtensions
{
    public static JsonReader MoveToContentAndAssert(this JsonReader reader)
    {
        if (reader == null)
            throw new ArgumentNullException();
        if (reader.TokenType == JsonToken.None)       // Skip past beginning of stream.
            reader.ReadAndAssert();
        while (reader.TokenType == JsonToken.Comment) // Skip past comments.
            reader.ReadAndAssert();
        return reader;
    }

    public static JsonReader ReadAndAssert(this JsonReader reader)
    {
        if (reader == null)
            throw new ArgumentNullException();
        if (!reader.Read())
            throw new JsonReaderException("Unexpected end of JSON stream.");
        return reader;
    }
}

Now, you have an additional requirement to serialize [Flags] enums, and only [Flags] enums, as integers. You can do this by further subclassing the above converter as follows:

public class ForceNumericFlagEnumConverter : FixedUlongStringEnumConverter
{
    static bool HasFlagsAttribute(Type objectType) 
    { 
        return Attribute.IsDefined(Nullable.GetUnderlyingType(objectType) ?? objectType, typeof(System.FlagsAttribute));
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        var enumType = value.GetType();
        if (HasFlagsAttribute(enumType))
        {
            var underlyingType = Enum.GetUnderlyingType(enumType);
            var underlyingValue = Convert.ChangeType(value, underlyingType);
            writer.WriteValue(underlyingValue);
        }
        else
        {
            base.WriteJson(writer, value, serializer);
        }
    }   
}

The workaround requires support for BigInteger which was introduced in .NET Framework 4.0.

Demo fiddle #2 here.

Upvotes: 1

Pranav Singh
Pranav Singh

Reputation: 20091

Change ulong to uint :

public enum UserCan : uint
{
    DoNothing = 0,
    ChangeConf =               1 << 0,
    MonitorOthers =              1 << 1,
    ForceOther =                1 << 2,
    EditRemoveOthers =           1 << 3,

    DoEverything = unchecked((uint)~0),
}

Working fiddle:

https://dotnetfiddle.net/DyQ2R7

Upvotes: 0

Related Questions