如何使用Newtonsoft JSON(新特软件JSON)序列化/反序列化XmlException?

8
这段示例代码:
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上创建了一个问题。


我也测试了一下,看起来像是一个 bug。 - chief7
谢谢chief7,这就是为什么我已经在Github上创建了一个问题。 - Fabian Vilers
生成的 JSON 的可读性有多重要? - dbc
可读性对于调试非常重要。 - Fabian Vilers
1个回答

4

问题

这里的基本问题是JSON和ISerializable + SerializationInfo之间的不兼容性,因为它们最初是设计用于与BinaryFormatter一起使用的,而该格式的流是强类型的。也就是说,ISerializable的实现有时候会期望序列化流包含序列化字段的完整类型信息。而事实证明XmlException也有这样一个实现。

具体如下。当Json.NET调用序列化构造函数来处理一个ISerializable类型时,它会构造一个SerializationInfo并传递一个JsonFormatterConverter,该转换器应该在调用SerializationInfo.GetValue(String, Type)时处理从JSON数据到所需类型的转换工作。现在,当未找到命名值时,此方法会抛出异常。不幸的是,没有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;
            }
        }

事实证明,此时的e.Value仍然是JValue而不是string,因此反序列化失败。

Json.NET可以通过在JsonSerializerInternalReader.CreateISerializable()中替换字符串类型的JValue标记为实际字符串来解决这个特定问题,在构造其SerializationInfo时,然后在JsonFormatterConverter中重新转换为JValue(如果需要转换)。但是,这不会解决这种类别的问题。例如,当int通过Json.NET进行往返传输时,它会变成long,如果没有转换就进行强制转换将抛出异常。当然,如果没有转换,DateTime字段也会抛出异常。这还会是一项破坏性的更改,因为之前手动设计用于与Json.NET配合使用的ISerializable类可能会失效。

我觉得你可以报告这个问题,但我怀疑它不会很快得到解决。

解决问题的更可靠方法是创建一个自定义 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 的可读性在一定程度上减轻了安全隐患。您还可以创建一个自定义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);
        }

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

感谢您的详细解释。我可能会暂时选择二进制序列化无法反序列化的类型,但这不是长期有效的解决方案 :( - Fabian Vilers

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接