Problem
The main problem here is the incompatibility between JSON, which is poorly typed, and ISerializabe + SerializationInfo , which were originally designed to work with BinaryFormatter , whose streams are heavily typed. That is, ISerializable implementations sometimes expect the serialization stream to contain complete type information for serialized fields. And it turns out that XmlException has one such implementation.
Specific features are as follows. When Json.NET accesses the serialization constructor for the ISerializable type, it builds a SerializationInfo and passes a JsonFormatterConverter , which should handle the work of converting from JSON data to the required type when SerializationInfo.GetValue(String, Type) called. Now this method throws an exception when the named value is not found. And, unfortunately, there is no SerializationInfo.TryGetValue() method that requires classes that need to deserialize the optional fields to cycle through the properties manually using GetEnumerator() , but there is also no way to get the converter installed in the constructor, which means that optional fields cannot be converted when necessary, so they had to be deserialized exactly with the expected type.
You can see this in the reference source for the XmlException constructor :
protected XmlException(SerializationInfo info, StreamingContext context) : base(info, context) { res = (string) info.GetValue("res" , typeof(string)); args = (string[])info.GetValue("args", typeof(string[])); lineNumber = (int) info.GetValue("lineNumber", typeof(int)); linePosition = (int) info.GetValue("linePosition", typeof(int));
It turns out that e.Value is still JValue not a string , so deserialization is throttled.
Json.NET can solve this specific problem: JsonSerializerInternalReader.CreateISerializable() , replacing JValue icons JValue string values ββwith actual strings when building it with SerializationInfo , and then converting it to JValue in JsonFormatterConverter if conversion is needed. However, this will not eliminate this category of problems. For example, when an int fires Json.NET with rounding to the end, it becomes long , which will throw if converted without conversion. And of course, the DateTime field will throw out without conversion. It would also be a change in that the ISerializable classes that were previously handled manually to work with Json.NET could break.
You can report this , but I will skeptically return to it in the near future.
A more robust approach to solving the problem is to create a custom JsonConverter that includes full type information for ISerializable types.
Solution 1: Insert Binary
The first, simplest solution is to insert a BinaryFormatter stream inside your JSON. The serialization code for Exception classes was originally designed to be compatible with BinaryFormatter , so this should be reasonably robust:
public class BinaryConverter<T> : JsonConverter where T : ISerializable { class BinaryData { public byte[] binaryData { get; set; } } public override bool CanConvert(Type objectType) { return typeof(T).IsAssignableFrom(objectType); } public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { if (reader.TokenType == JsonToken.Null) return null; var data = serializer.Deserialize<BinaryData>(reader); if (data == null || data.binaryData == null) return null; return BinaryFormatterHelper.FromByteArray<T>(data.binaryData); } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { var data = new BinaryData { binaryData = BinaryFormatterHelper.ToByteArray(value) }; serializer.Serialize(writer, data); } } public static class BinaryFormatterHelper { public static byte [] ToByteArray<T>(T obj) { using (var stream = new MemoryStream()) { new BinaryFormatter().Serialize(stream, obj); return stream.ToArray(); } } public static T FromByteArray<T>(byte[] data) { return FromByteArray<T>(data, null); } public static T FromByteArray<T>(byte[] data, BinaryFormatter formatter) { using (var stream = new MemoryStream(data)) { formatter = (formatter ?? new BinaryFormatter()); var obj = formatter.Deserialize(stream); if (obj is T) return (T)obj; return default(T); } } }
And then serialize with the following settings:
var settings = new JsonSerializerSettings { Converters = new[] { new BinaryConverter<Exception>() } };
Disadvantages:
There is a serious danger for deserializing untrusted data. Since type information is fully embedded within a proprietary, unreadable serialization stream, you may not know what you are going to build until you have done so.
JSON is not fully readable.
I believe that BinaryFormatter missing in some versions of .Net.
I believe that BinaryFormatter can only be used in full trust.
But if all you are trying to do is serialize the exception between processes under your control, that might be good enough.
Solution 2. Insert type information using TypeNameHandling .
Json.NET also has the optional ability to insert .NET type information for non-primitive types in the serialization stream by setting JsonSerializer.TypeNameHandling to the appropriate value. Using this ability along with wrappers for primitive types, you can create a JsonConverter that encapsulates SerializationInfo and SerializationEntry and contains all the known type information:
public class ISerializableConverter<T> : JsonConverter where T : ISerializable { public override bool CanConvert(Type objectType) { return typeof(T).IsAssignableFrom(objectType); } public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { if (reader.TokenType == JsonToken.Null) return null; var oldTypeNameHandling = serializer.TypeNameHandling; var oldAssemblyFormat = serializer.TypeNameAssemblyFormat; try { if (serializer.TypeNameHandling == TypeNameHandling.None) serializer.TypeNameHandling = TypeNameHandling.Auto; else if (serializer.TypeNameHandling == TypeNameHandling.Arrays) serializer.TypeNameHandling = TypeNameHandling.All; var data = serializer.Deserialize<SerializableData>(reader); var type = data.ObjectType; var info = new SerializationInfo(type, new FormatterConverter()); foreach (var item in data.Values) info.AddValue(item.Key, item.Value.ObjectValue, item.Value.ObjectType); var value = Activator.CreateInstance(type, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, null, new object[] { info, serializer.Context }, serializer.Culture); if (value is IObjectReference) value = ((IObjectReference)value).GetRealObject(serializer.Context); return value; } finally { serializer.TypeNameHandling = oldTypeNameHandling; serializer.TypeNameAssemblyFormat = oldAssemblyFormat; } } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { var oldTypeNameHandling = serializer.TypeNameHandling; var oldAssemblyFormat = serializer.TypeNameAssemblyFormat; try { var serializable = (ISerializable)value; var context = serializer.Context; var info = new SerializationInfo(value.GetType(), new FormatterConverter()); serializable.GetObjectData(info, context); var data = SerializableData.CreateData(info, value.GetType()); if (serializer.TypeNameHandling == TypeNameHandling.None) serializer.TypeNameHandling = TypeNameHandling.Auto; else if (serializer.TypeNameHandling == TypeNameHandling.Arrays) serializer.TypeNameHandling = TypeNameHandling.All; // The following seems to be required by https://github.com/JamesNK/Newtonsoft.Json/issues/787 serializer.TypeNameAssemblyFormat = System.Runtime.Serialization.Formatters.FormatterAssemblyStyle.Full; serializer.Serialize(writer, data, typeof(SerializableData)); } finally { serializer.TypeNameHandling = oldTypeNameHandling; serializer.TypeNameAssemblyFormat = oldAssemblyFormat; } } } abstract class SerializableValue { [JsonIgnore] public abstract object ObjectValue { get; } [JsonIgnore] public abstract Type ObjectType { get; } public static SerializableValue CreateValue(SerializationEntry entry) { return CreateValue(entry.ObjectType, entry.Value); } public static SerializableValue CreateValue(Type type, object value) { if (value == null) { if (type == null) throw new ArgumentException("type and value are both null"); return (SerializableValue)Activator.CreateInstance(typeof(SerializableValue<>).MakeGenericType(type)); } else { type = value.GetType(); // Use most derived type return (SerializableValue)Activator.CreateInstance(typeof(SerializableValue<>).MakeGenericType(type), value); } } } sealed class SerializableValue<T> : SerializableValue { public SerializableValue() : base() { } public SerializableValue(T value) : base() { this.Value = value; } public override object ObjectValue { get { return Value; } } public override Type ObjectType { get { return typeof(T); } } [JsonProperty("value", NullValueHandling = NullValueHandling.Ignore)] public T Value { get; private set; } } abstract class SerializableData { public SerializableData() { this.Values = new Dictionary<string, SerializableValue>(); } public SerializableData(IEnumerable<SerializationEntry> values) { this.Values = values.ToDictionary(v => v.Name, v => SerializableValue.CreateValue(v)); } [JsonProperty("values", ItemTypeNameHandling = TypeNameHandling.Auto)] public Dictionary<string, SerializableValue> Values { get; private set; } [JsonIgnore] public abstract Type ObjectType { get; } public static SerializableData CreateData(SerializationInfo info, Type initialType) { if (info == null) throw new ArgumentNullException("info"); var type = info.GetSavedType(initialType); if (type == null) throw new InvalidOperationException("type == null"); return (SerializableData)Activator.CreateInstance(typeof(SerializableData<>).MakeGenericType(type), info.AsEnumerable()); } } sealed class SerializableData<T> : SerializableData { public SerializableData() : base() { } public SerializableData(IEnumerable<SerializationEntry> values) : base(values) { } public override Type ObjectType { get { return typeof(T); } } } public static class SerializationInfoExtensions { public static IEnumerable<SerializationEntry> AsEnumerable(this SerializationInfo info) { if (info == null) throw new NullReferenceException(); var enumerator = info.GetEnumerator(); while (enumerator.MoveNext()) { yield return enumerator.Current; } } public static Type GetSavedType(this SerializationInfo info, Type initialType) { if (initialType != null) { if (info.FullTypeName == initialType.FullName && info.AssemblyName == initialType.Module.Assembly.FullName) return initialType; } var assembly = Assembly.Load(info.AssemblyName); if (assembly != null) { var type = assembly.GetType(info.FullTypeName); if (type != null) return type; } return initialType; } }
And then use the following settings:
This gives semi-readable JSON as shown below:
{ "$type": "Question35015357.SerializableData`1[[System.Xml.XmlException, System.Xml, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], Tile, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", "values": { "ClassName": { "$type": "Question35015357.SerializableValue`1[[System.String, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], Tile, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", "value": "System.Xml.XmlException" }, "Message": { "$type": "Question35015357.SerializableValue`1[[System.String, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], Tile, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", "value": "bla" }, "Data": { "$type": "Question35015357.SerializableValue`1[[System.Collections.IDictionary, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], Tile, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" }, "InnerException": { "$type": "Question35015357.SerializableValue`1[[System.Exception, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], Tile, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" }, "HelpURL": { "$type": "Question35015357.SerializableValue`1[[System.String, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], Tile, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" }, "StackTraceString": { "$type": "Question35015357.SerializableValue`1[[System.String, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], Tile, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" }, "RemoteStackTraceString": { "$type": "Question35015357.SerializableValue`1[[System.String, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], Tile, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" }, "RemoteStackIndex": { "$type": "Question35015357.SerializableValue`1[[System.Int32, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], Tile, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", "value": 0 }, "ExceptionMethod": { "$type": "Question35015357.SerializableValue`1[[System.String, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], Tile, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" }, "HResult": { "$type": "Question35015357.SerializableValue`1[[System.Int32, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], Tile, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", "value": -2146232000 }, "Source": { "$type": "Question35015357.SerializableValue`1[[System.String, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], Tile, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" }, "res": { "$type": "Question35015357.SerializableValue`1[[System.String, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], Tile, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", "value": "Xml_UserException" }, "args": { "$type": "Question35015357.SerializableValue`1[[System.String[], mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], Tile, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", "value": [ "bla" ] }, "lineNumber": { "$type": "Question35015357.SerializableValue`1[[System.Int32, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], Tile, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", "value": 0 }, "linePosition": { "$type": "Question35015357.SerializableValue`1[[System.Int32, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], Tile, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", "value": 0 }, "sourceUri": { "$type": "Question35015357.SerializableValue`1[[System.Object, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], Tile, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" }, "version": { "$type": "Question35015357.SerializableValue`1[[System.String, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], Tile, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", "value": "2.0" } } }
As you can see, the security risk is somewhat mitigated by the readability of JSON. You can also create a custom SerializationBinder to further reduce the security risk when loading only the expected types, as described in the TypeNameHandling warning in Newtonsoft Json .
I am not sure what to do in situations with partial trust. JsonSerializerInternalReader.CreateISerializable() throws partial trust:
private object CreateISerializable(JsonReader reader, JsonISerializableContract contract, JsonProperty member, string id) { Type objectType = contract.UnderlyingType; if (!JsonTypeReflector.FullyTrusted) { string message = @"Type '{0}' implements ISerializable but cannot be deserialized using the ISerializable interface because the current application is not fully trusted and ISerializable can expose secure data." + Environment.NewLine + @"To fix this error either change the environment to be fully trusted, change the application to not deserialize the type, add JsonObjectAttribute to the type or change the JsonSerializer setting ContractResolver to use a new DefaultContractResolver with IgnoreSerializableInterface set to true." + Environment.NewLine; message = message.FormatWith(CultureInfo.InvariantCulture, objectType); throw JsonSerializationException.Create(reader, message); }
So maybe a converter too.