如何使用Newtonsoft JSON(de)序列化XmlException?

此示例代码:

var json = JsonConvert.SerializeObject(new XmlException("bla")); var exception = JsonConvert.DeserializeObject(json); 

在Newtonsoft.Json.dll中抛出InvalidCastException:无法将类型为“Newtonsoft.Json.Linq.JValue”的对象强制转换为使用以下堆栈跟踪键入“System.String”:

 at System.Xml.XmlException..ctor(SerializationInfo info, StreamingContext context) at Void .ctor(System.Runtime.Serialization.SerializationInfo, System.Runtime.Serialization.StreamingContext)(Object[] ) at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateISerializable(JsonReader reader, JsonISerializableContract contract, JsonProperty member, String id) at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateObject(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue) at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateValueInternal(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue) at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.Deserialize(JsonReader reader, Type objectType, Boolean checkAdditionalContent) at Newtonsoft.Json.JsonSerializer.DeserializeInternal(JsonReader reader, Type objectType) at Newtonsoft.Json.JsonConvert.DeserializeObject(String value, Type type, JsonSerializerSettings settings) at Newtonsoft.Json.JsonConvert.DeserializeObject[T](String value, JsonSerializerSettings settings) at Newtonsoft.Json.JsonConvert.DeserializeObject[T](String value) at TestJson.Program.Main(String[] args) in C:\Projects\TestJson\TestJson\Program.cs:line 21 at System.AppDomain._nExecuteAssembly(RuntimeAssembly assembly, String[] args) at System.AppDomain.ExecuteAssembly(String assemblyFile, Evidence assemblySecurity, String[] args) at Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly() at System.Threading.ThreadHelper.ThreadStart_Context(Object state) at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx) at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx) at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state) at System.Threading.ThreadHelper.ThreadStart() 

我错过了什么吗?

在https://github.com/JamesNK/Newtonsoft.Json/issues/801上创建了一个问题

问题

这里的基本问题是弱类型的JSON和ISerializabe + SerializationInfo之间的不兼容性,它最初设计用于BinaryFormatter其流是强类型的。 即ISerializable实现有时期望序列化流包含序列化字段的完整类型信息 。 事实certificate, XmlException有一个这样的实现。

具体如下。 当Json.NET为ISerializable类型调用序列化构造函数时 ,它构造一个SerializationInfo并传递一个JsonFormatterConverter ,它应该在调用SerializationInfo.GetValue(String, Type)时处理从JSON数据转换为所需类型的工作。 现在,此方法在找不到指定值时抛出exception。 而且,遗憾的是,没有SerializationInfo.TryGetValue()方法 ,要求需要反序列化可选字段的类以使用GetEnumerator()手动遍历属性。 但是,此外, 没有方法可以在构造函数中检索转换器集 ,这意味着在需要时无法转换可选字段,因此必须使用精确的预期类型对其进行反序列化。

您可以在XmlException的构造函数的引用源中看到这一点:

  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)); // deserialize optional members sourceUri = string.Empty; string version = null; foreach ( SerializationEntry e in info ) { switch ( e.Name ) { case "sourceUri": sourceUri = (string)e.Value; break; case "version": version = (string)e.Value; break; } } 

事实certificate, e.Value仍然是JValue而不是string ,因此反序列化会阻塞。

Json.NET可以通过在JsonSerializerInternalReader.CreateISerializable()修复此特定问题,在构造其SerializationInfo时用实际字符串替换字符串值JValue标记,然后如果需要转换则在JsonFormatterConverter重新转换为JValue 。 但是,这不会解决这类问题。 例如,当一个int被Json.NET往返时,它变成一个long ,如果没有转换就会抛出。 当然, DateTime字段将在没有转换的情况下抛出。 这也是一个突破性的变化,以前手工制作与Json.NET合作的ISerializable类可能会破坏。

您可能会报告此问题 ,但我怀疑它会很快得到解决。

解决问题的一种更强大的方法是创建一个自定义JsonConverter ,它嵌入ISerializable类型的完整类型信息。

解决方案1:嵌入二进制文件

第一个最简单的解决方案是在JSON中嵌入BinaryFormatter流。 Exception类的序列化代码最初设计为与BinaryFormatter兼容,因此这应该是相当可靠的:

 public class BinaryConverter : 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(reader); if (data == null || data.binaryData == null) return null; return BinaryFormatterHelper.FromByteArray(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 obj) { using (var stream = new MemoryStream()) { new BinaryFormatter().Serialize(stream, obj); return stream.ToArray(); } } public static T FromByteArray(byte[] data) { return FromByteArray(data, null); } public static T FromByteArray(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); } } } 

然后使用以下设置进行序列化:

 var settings = new JsonSerializerSettings { Converters = new[] { new BinaryConverter() } }; 

缺点是:

  1. 将不受信任的数据反序列化存在严重的安全隐患。 由于类型信息完全嵌入在专有的,不可读的序列化流中,因此在完成之前,您无法知道要构建的内容。

  2. JSON完全不可读。

  3. 我相信某些.Net版本缺少BinaryFormatter

  4. 我相信BinaryFormatter只能用于完全信任。

但是,如果您要做的就是在您控制的进程之间序列化exception,这可能就足够了。

解决方案2:使用TypeNameHandling嵌入类型信息

通过将JsonSerializer.TypeNameHandling设置为适当的值,Json.NET还具有为序列化流中的非基本类型嵌入.NET类型信息的可选function。 使用此function以及原始类型的包装器,可以创建一个封装SerializationInfoSerializationEntry并包含所有已知类型信息的JsonConverter

 public class ISerializableConverter : 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(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 : 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(); } public SerializableData(IEnumerable values) { this.Values = values.ToDictionary(v => v.Name, v => SerializableValue.CreateValue(v)); } [JsonProperty("values", ItemTypeNameHandling = TypeNameHandling.Auto)] public Dictionary 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 : SerializableData { public SerializableData() : base() { } public SerializableData(IEnumerable values) : base(values) { } public override Type ObjectType { get { return typeof(T); } } } public static class SerializationInfoExtensions { public static IEnumerable 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; } } 

然后使用以下设置:

这会生成半可读的JSON,如下所示:

 { "$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" } } } 

如您所见,JSON的可读性在一定程度上减轻了安全隐患。 您还可以创建自定义SerializationBinder以进一步降低加载预期类型的​​安全隐患,如Newtonsoft Json中的TypeNameHandling警告中所述

我不确定在部分信任情况下应该做些什么。 JsonSerializerInternalReader.CreateISerializable()抛出部分信任:

  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); } 

所以也许转换器也应如此。