I have a custom Exception :
[Serializable]
public class MyCustomException : Exception
{
public List<ErrorInfo> ErrorInfoList { get; set; }
protected MyCustomException (SerializationInfo info, StreamingContext context)
: base(info, context)
{
this.ErrorInfoList = (List<ErrorInfo>)info.GetValue("ErrorInfoList", typeof(List<ErrorInfo>));
}
[SecurityPermissionAttribute(SecurityAction.Demand, SerializationFormatter = true)]
public override void GetObjectData(SerializationInfo info, StreamingContext context)
{
if (info == null)
{
throw new ArgumentNullException("info");
}
info.AddValue("ErrorInfoList ", this.ErrorInfoList, typeof(List<ErrorInfo>));
base.GetObjectData(info, context);
}
}
Whenever it tries to deserialize, this line throws an "Object must implement IConvertible" exception: (List<ErrorInfo>)info.GetValue("ErrorInfoList", typeof(List<ErrorInfo>))
Here's the bit of code that does the serialization:
using(MemoryStream memStm = new MemoryStream())
{
XmlObjectSerializer ser = new DataContractJsonSerializer(
typeof(MyCustomException),
new Type[] {
typeof(List<ErrorInfo>),
typeof(ErrorInfo)
}
);
ser.WriteObject(memStm, (MyCustomException)context.Exception);
memStm.Seek(0, SeekOrigin.Begin);
using (StreamReader streamReader = new StreamReader(memStm))
{
response.Content = new StringContent(streamReader.ReadToEnd());
}
}
Here's the bit of code that does the deserialization:
using(MemoryStream memStm = new MemoryStream(response.Content.ReadAsByteArrayAsync().Result))
{
DataContractJsonSerializer deserializer = new DataContractJsonSerializer(
typeof(MyCustomException),
new Type[] {
typeof(List<ErrorInfo>),
typeof(ErrorInfo)
}
);
UserPortalException upEx = (UserPortalException)deserializer.ReadObject(memStm);
throw upEx;
}
Here's the code for the ErrorInfo class:
[Serializable]
public class ErrorInfo : ISerializable
{
public enum ErrorCode {
[.....]
}
public ErrorCode Code { get; set; }
public void GetObjectData(SerializationInfo info, StreamingContext context)
{
info.AddValue("Code", this.Code , typeof(ErrorCode ));
}
public Error(SerializationInfo info, StreamingContext context)
{
this.Code = (ErrorCode)Enum.Parse(typeof(ErrorCode), info.GetInt32("Code").ToString());
}
}
The basic problem here is is that the ISerializable
interface was originally designed (in .Net 1) to work with BinaryFormatter
. And, while BinaryFormatter
serialization streams contain complete type information, JSON is weakly typed. This causes problems described in Stand-Alone JSON Serialization:
Supported and Unsupported ISerializable Types
In general, types that implement the ISerializable interface are fully supported when serializing/deserializing JSON. However, some of these types (including some .NET Framework types) are implemented in such a way that the JSON-specific serialization aspects cause them to not deserialize correctly:
- With ISerializable, the type of individual data members is never known in advance. This leads to a polymorphic situation similar to deserializing types into an object. As mentioned before, this may lead to loss of type information in JSON. For example, a type that serializes an enum in its ISerializable implementation and attempts to deserialize back directly into an enum (without proper casts) fails, because an enum is serialized using numbers in JSON and JSON numbers deserialize into built-in .NET numeric types (Int32, Decimal or Double). So the fact that the number used to be an enum value is lost.
What you are experiencing is just such a loss of type information. If you look at the JSON generated for your custom exception, you will see:
{"ErrorInfoList":[{"__type":"ErrorInfo:#Question40048102","Code":0}],"ClassName":"Question40048102.MyCustomException","Message":null,"Data":null,"InnerException":null,"HelpURL":null,"StackTraceString":null,"RemoteStackTraceString":null,"RemoteStackIndex":0,"ExceptionMethod":null,"HResult":-2146233088,"Source":null}
There is a "__type"
type hint for each ErrorInfo
, but no type hint for the ErrorInfoList
, because DataContractJsonSerializer
does not support type hints for collections. Thus, the ErrorInfoList
gets deserialized as an object []
array containing ErrorInfo
objects rather than a List<ErrorInfo>
, leading to the error you see.
So, in principle, you could change your initialization of ErrorInfoList
as follows:
this.ErrorInfoList = ((IEnumerable<object>)info.GetValue("ErrorInfoList", typeof(object []))).Cast<ErrorInfo>().ToList();
However, this would break binary and XML data contract deserialization where the entry value is already typed correctly. It would also break Json.NET deserialization which uses a completely different mechanism, namely storing JToken
values inside the SerializationInfo
and deserializing on demand using a custom IFormatterConverter
.
Thus a bit of code smell is required to support all of the above serializers:
[Serializable]
[KnownType(typeof(List<ErrorInfo>))]
[KnownType(typeof(ErrorInfo))]
public class MyCustomException : Exception
{
public List<ErrorInfo> ErrorInfoList { get; set; }
public MyCustomException()
: base()
{
this.ErrorInfoList = new List<ErrorInfo>();
}
protected MyCustomException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
foreach (SerializationEntry entry in info)
{
if (entry.Name == "ErrorInfoList")
{
if (entry.Value == null)
this.ErrorInfoList = null;
else
{
if (entry.Value is List<ErrorInfo>)
{
// Already fully typed (BinaryFormatter and DataContractSerializer)
this.ErrorInfoList = (List<ErrorInfo>)entry.Value;
}
else if (entry.Value is IEnumerable && !(entry.Value is string))
{
var enumerable = (IEnumerable)entry.Value;
if (!enumerable.OfType<object>().Any())
{
// Empty collection
this.ErrorInfoList = new List<ErrorInfo>();
}
else if (enumerable.OfType<ErrorInfo>().Any())
{
// Collection is untyped but entries are typed (DataContractJsonSerializer)
this.ErrorInfoList = enumerable.OfType<ErrorInfo>().ToList();
}
}
if (this.ErrorInfoList == null)
{
// Entry value not already deserialized into a collection (typed or untyped) of ErrorInfo instances (json.net).
// Let the supplied formatter converter do the conversion.
this.ErrorInfoList = (List<ErrorInfo>)info.GetValue("ErrorInfoList", typeof(List<ErrorInfo>));
}
}
}
}
}
[SecurityPermissionAttribute(SecurityAction.Demand, SerializationFormatter = true)]
public override void GetObjectData(SerializationInfo info, StreamingContext context)
{
if (info == null)
{
throw new ArgumentNullException("info");
}
info.AddValue("ErrorInfoList", this.ErrorInfoList, typeof(List<ErrorInfo>));
base.GetObjectData(info, context);
}
}
[Serializable]
[KnownType(typeof(ErrorInfo.ErrorCode))]
public class ErrorInfo : ISerializable
{
public enum ErrorCode
{
One,
Two
}
public ErrorCode Code { get; set; }
public void GetObjectData(SerializationInfo info, StreamingContext context)
{
info.AddValue("Code", this.Code, typeof(ErrorCode));
}
public ErrorInfo() { }
protected ErrorInfo(SerializationInfo info, StreamingContext context)
{
this.Code = (ErrorCode)Enum.Parse(typeof(ErrorCode), info.GetInt32("Code").ToString());
}
}