【问题标题】:How to (de)serialize a XmlException with Newtonsoft JSON?如何使用 Newtonsoft JSON(反)序列化 XmlException?
【发布时间】:2016-01-26 13:50:19
【问题描述】:

此示例代码:

var json = JsonConvert.SerializeObject(new XmlException("bla"));
var exception = JsonConvert.DeserializeObject<XmlException>(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 创建了一个问题

【问题讨论】:

  • 我也对此进行了测试,对我来说它看起来像是一个错误。
  • 感谢 Chief7,这就是为什么我已经在 Github 上创建了一个问题。
  • 生成的 JSON 的可读性有多重要?
  • 可读性对于调试目的很重要。

标签: c# json json.net


【解决方案1】:

问题

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

具体如下。当 Json.NET 为 ISerializable 类型调用 serialization constructor 时,它 constructs a SerializationInfo 并传递一个 JsonFormatterConverter,它应该在调用 SerializationInfo.GetValue(String, Type) 时处理从 JSON 数据转换为所需类型的工作。现在,当未找到命名值时,此方法会引发异常。而且,不幸的是,没有SerializationInfo.TryGetValue() 方法,需要需要反序列化可选字段的类才能使用GetEnumerator() 手动循环属性。但除此之外,没有方法可以在构造函数中检索转换器集,这意味着可选字段在需要时无法转换,因此必须已将所需的字段精确地反序列化为预期的类型。

你可以在reference source for the constructor of 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;
            }
        }

现在e.Value 仍然是JValue 而不是string,所以反序列化会阻塞。

Json.NET 可以解决这个特定问题,在JsonSerializerInternalReader.CreateISerializable() 中,在构造SerializationInfo 时用实际字符串替换字符串值JValue 标记,然后再转换为@ 987654360@ in JsonFormatterConverter 如果需要转换。然而,这并不能解决这个类别的问题。例如,当 int 被 Json.NET 往返时,它变成了 long,如果没有转换就会抛出。当然,DateTime 字段将在没有转换的情况下抛出。这也是一个突破性的变化,以前手工制作以与 Json.NET 一起使用的 ISerializable 类可能会损坏。

您可能会report an issue 对此,但我怀疑它会很快得到解决。

解决该问题的更稳健的方法是创建一个自定义 JsonConverter,其中嵌入了 ISerializable 类型的完整类型信息。

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

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

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

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

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

缺点是:

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

  2. JSON 完全不可读。

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

  4. 我相信BinaryFormatter只能在完全信任的情况下使用。

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

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

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

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

然后使用以下设置:

这会生成如下所示的半可读 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 的可读性在一定程度上缓解了安全隐患。您还可以创建一个custom SerializationBinder 以进一步减少仅加载预期类型的​​安全隐患,如TypeNameHandling caution in Newtonsoft Json 中所述。

我不确定在部分信任的情况下应该怎么做。 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);
        }

所以也许转换器也应该这样做。

【讨论】:

  • 感谢您的详细解释。我可能会对暂时无法反序列化的类型进行二进制序列化,但这不是长期有效的解决方案:(
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-06-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多