Reputation: 1250
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
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
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