从 XML 文档创建的 Json 反序列化到 POCO 不适用于数组

Posted

技术标签:

【中文标题】从 XML 文档创建的 Json 反序列化到 POCO 不适用于数组【英文标题】:Deserialization to POCO from Json created of XML document does not work with arrays 【发布时间】:2021-10-15 18:49:42 【问题描述】:

我有以下 C# 类

[DataContract(Name = "Person")]
public sealed class Person

    [DataMember]
    public string Name  get; set; 

    [DataMember]
    public List<int> Numbers  get; set; 

创建的对象

Person person = new Person

    Name = "Test",
    Numbers = new List<int>  1, 2, 3 
;

被序列化为带有DataContractSerializer的XML文档:

<Person xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/Workflows.MassTransit.Hosting.Serialization">
    <Name>Test</Name>
    <Numbers xmlns:d2p1="http://schemas.microsoft.com/2003/10/Serialization/Arrays">
        <d2p1:int>1</d2p1:int>
        <d2p1:int>2</d2p1:int>
        <d2p1:int>3</d2p1:int>
    </Numbers>
</Person>

JsonConvert.SerializeXNode(xmlDocument) 返回以下 JSON:


    "Person": 
        "@xmlns:i": "http://www.w3.org/2001/XMLSchema-instance",
        "@xmlns": "http://schemas.datacontract.org/2004/07/Workflows.MassTransit.Hosting.Serialization",
        "Name": "Test",
        "Numbers": 
            "@xmlns:d2p1": "http://schemas.microsoft.com/2003/10/Serialization/Arrays",
            "d2p1:int": [
                "1",
                "2",
                "3"
            ]
        
    

当我使用 JsonConvert.DeserializeObject(json, typeof(Person)) 将上面的 JSON 反序列化为 POCO 时,两个属性(NameNumbers)都是 NULL

我已经尝试移除外部对象:


    "@xmlns:i": "http://www.w3.org/2001/XMLSchema-instance",
    "@xmlns": "http://schemas.datacontract.org/2004/07/Workflows.MassTransit.Hosting.Serialization",
    "Name": "Test",
    "Numbers": 
        "@xmlns:d2p1": "http://schemas.microsoft.com/2003/10/Serialization/Arrays",
        "d2p1:int": [
            "1",
            "2",
            "3"
        ]
    

然后出现以下异常:

Newtonsoft.Json.JsonSerializationException
  HResult=0x80131500
  Nachricht = Cannot deserialize the current JSON object (e.g. "name":"value") into type 'System.Collections.Generic.List`1[System.Int32]' because the type requires a JSON array (e.g. [1,2,3]) to deserialize correctly.
To fix this error either change the JSON to a JSON array (e.g. [1,2,3]) or change the deserialized type so that it is a normal .NET type (e.g. not a primitive type like integer, not a collection type like an array or List<T>) that can be deserialized from a JSON object. JsonObjectAttribute can also be added to the type to force it to deserialize from a JSON object.
Path 'Numbers.@xmlns:d2p1', line 6, position 18.
  Quelle = Newtonsoft.Json
  Stapelüberwachung:
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateObject(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue)

从 XML 文档转换而来的 JSON 似乎与从 POCO 直接创建的 JSON 不同。

POCO => JSON => POCO 工作正常

POCO => XML => JSON => POCO POCO 获得 NULL 属性和/或异常

是否可以配置 Json.NET 以创建兼容的 JSON 文档?

【问题讨论】:

你在为自己做不必要的工作。只要将Namespace = "http://schemas.datacontract.org/2004/07/Workflows.MassTransit.Hosting.Serialization" 添加到DataContractAttribute,就可以使用DataContractSerializer 将XML 直接反序列化为Person,请参阅dotnetfiddle.net/Q46exx。为什么要转码为 JSON,然后使用 Json.NET 反序列化? 【参考方案1】:

对于某些List&lt;T&gt;,您可以编写一个custom JsonConverter 来反序列化从DataContractSerializer 生成的XML 转码的JSON,但它会非常复杂。考虑到 Newtonsoft 文档Converting between JSON and XML 中指定的“转换规则”:

由于同一级别的多个同名节点被组合成一个数组,因此转换过程可以根据节点的数量产生不同的JSON。

以下四种情况必须处理:

    包含多个项目的列表值如下所示:

      
        "@xmlns:d2p1": "http://schemas.microsoft.com/2003/10/Serialization/Arrays",
        "d2p1:int": [
          "1",
          "2",
          "3"
        ]
      
    

    或(没有命名空间):

      
        "int": [
          "1",
          "2",
          "3"
        ]
      
    

    具有单个值的列表如下所示:

      
        "@xmlns:d2p1": "http://schemas.microsoft.com/2003/10/Serialization/Arrays",
        "d2p1:int": "1"
      
    

    或(没有命名空间):

      
        "int": "1"
      
    

    空列表如下所示:

      
        "@xmlns:d2p1": "http://schemas.microsoft.com/2003/10/Serialization/Arrays"
      
    

    或(无命名空间):

      null
    

    (是的,这是&lt;Numbers /&gt;的正确转码。)

    null 列表如下所示:

      
        "@xmlns:d2p1": "http://schemas.microsoft.com/2003/10/Serialization/Arrays",
        "@i:nil": "true"
      
    

    或者

      
        "@i:nil": "true"
      
    

对于任何T 本身不是集合​​的所有List&lt;T&gt; 值,以下转换器应处理所有情况:

public class ListFromDataContractXmlConverter : JsonConverter

    readonly IContractResolver resolver;

    public ListFromDataContractXmlConverter() : this(null)  

    public ListFromDataContractXmlConverter(IContractResolver resolver)
    
        // Use the global default resolver if none is passed in.
        this.resolver = resolver ?? new JsonSerializer().ContractResolver;
    

    static bool CanConvert(Type objectType, IContractResolver resolver)
    
        Type itemType;
        JsonArrayContract contract;
        return CanConvert(objectType, resolver, out itemType, out contract);
    

    static bool CanConvert(Type objectType, IContractResolver resolver, out Type itemType, out JsonArrayContract contract)
    
        if ((itemType = objectType.GetListItemType()) == null)
        
            itemType = null;
            contract = null;
            return false;
        
        // Ensure that [JsonObject] is not applied to the type.
        if ((contract = resolver.ResolveContract(objectType) as JsonArrayContract) == null)
            return false;
        var itemContract = resolver.ResolveContract(itemType);
        // Not implemented for jagged arrays.
        if (itemContract is JsonArrayContract)
            return false;
        return true;
    

    public override bool CanConvert(Type objectType)  return CanConvert(objectType, resolver); 

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    
        Type itemType;
        JsonArrayContract contract;

        if (!CanConvert(objectType, serializer.ContractResolver, out itemType, out contract))
            throw new JsonSerializationException(string.Format("Invalid type for 0: 1", GetType(), objectType));
        var list = (IList)(existingValue ?? contract.DefaultCreator());
        switch (reader.MoveToContentAndAssert().TokenType)
        
            case JsonToken.Null:
                // This is how Json.NET transcodes an empty DataContractSerializer XML list when no namespace is present.
                break;
            case JsonToken.StartArray:
                
                    serializer.Populate(reader, list);
                
                break;
            case JsonToken.StartObject:
                while (reader.ReadToContentAndAssert().TokenType != JsonToken.EndObject)
                
                    var name = (string)reader.AssertTokenType(JsonToken.PropertyName).Value;
                    reader.ReadToContentAndAssert();
                    if (name.StartsWith("@"))
                    
                        // It's an attribute.
                        if (!name.StartsWith("@xmlns:") && name.EndsWith("nil") && reader.TokenType == JsonToken.String && ((string)reader.Value) == "true")
                            // It's not a namespace, it's the http://www.w3.org/2001/XMLSchema-instancenil="true" null indicator attribute.
                            list = null;
                    
                    else
                    
                        // It's an element.
                        if (reader.TokenType == JsonToken.StartArray)
                            // The list had multiple items
                            serializer.Populate(reader, list);
                        else
                            // The list had a single item. This is it.
                            list.Add(serializer.Deserialize(reader, itemType));
                    
                
                break;
            default:
                throw new JsonSerializationException(string.Format("Unexpected token 0", reader.TokenType));
        
        return list;
    

    public override bool CanWrite => false;

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) => throw new NotImplementedException();


public static partial class JsonExtensions

    public static JsonReader AssertTokenType(this JsonReader reader, JsonToken tokenType) => 
        reader.TokenType == tokenType ? reader : throw new JsonSerializationException(string.Format("Unexpected token 0, expected 1", reader.TokenType, tokenType));
    
    public static JsonReader ReadToContentAndAssert(this JsonReader reader) =>
        reader.ReadAndAssert().MoveToContentAndAssert();

    public static JsonReader MoveToContentAndAssert(this JsonReader reader)
    
        if (reader == null)
            throw new ArgumentNullException();
        if (reader.TokenType == JsonToken.None)       // Skip past beginning of stream.
            reader.ReadAndAssert();
        while (reader.TokenType == JsonToken.Comment) // Skip past comments.
            reader.ReadAndAssert();
        return reader;
    

    public static JsonReader ReadAndAssert(this JsonReader reader)
    
        if (reader == null)
            throw new ArgumentNullException();
        if (!reader.Read())
            throw new JsonReaderException("Unexpected end of JSON stream.");
        return reader;
    

    public static Type GetListItemType(this Type type)
    
        // Quick reject for performance
        if (type.IsPrimitive || type.IsArray || type == typeof(string))
            return null;
        while (type != null)
        
            if (type.IsGenericType)
            
                var genType = type.GetGenericTypeDefinition();
                if (genType == typeof(List<>))
                    return type.GetGenericArguments()[0];
            
            type = type.BaseType;
        
        return null;
    

然后,要转码和反序列化,请执行以下操作:

var xelement = XElement.Parse(xmlString);
var jsonFromXml = JsonConvert.SerializeXNode(xelement, Formatting.None, omitRootObject: true);

var settings = new JsonSerializerSettings

    Converters =  new ListFromDataContractXmlConverter() ,
;

var deserializedPerson = JsonConvert.DeserializeObject<Person>(jsonFromXml, settings);

注意事项:

转换器不适用于锯齿状列表(即任何List&lt;T&gt;,其中T 是一些IEnumerable 其他字符串,例如List&lt;List&lt;int&gt;&gt;List&lt;int []&gt;)。

要实现锯齿状列表,有必要将包含一个具有多个值的项目的 JSON 与包含多个项目的列表的 JSON 与每个具有一个值的列表的 JSON 与一个本身具有一个值的列表进行比较具有一个值,并确保无论是否存在命名空间都可以成功区分这些情况。

如果没有omitRootObject: true,中间 JSON 将有一个额外级别的对象嵌套

 "Person":  /* The person contents */  

您需要向数据模型添加额外级别的包装器对象以反序列化此类 JSON,例如:

public class RootObject  public Person Person  get; set;  

解析为 XElement 而不是 XDocument 会从转码的 JSON 中去除 XML 声明,否则在使用 omitRootObject: true 反序列化时可能会导致问题。

使用 DataContractSerializer 直接从 XML 反序列化 Person 会容易得多。只要您将正确的命名空间添加到数据协定属性中,您就可以执行此操作(仅当它与您的 default namespace 不同时才需要这样做):

[DataContract(Name = "Person", Namespace = "http://schemas.datacontract.org/2004/07/Workflows.MassTransit.Hosting.Serialization")]
public sealed class Person

    [DataMember]
    public string Name  get; set; 

    [DataMember]
    public List<int> Numbers  get; set; 

请参阅https://dotnetfiddle.net/Q46exx 以获取演示。

这里的演示小提琴:https://dotnetfiddle.net/9B7Sdn。

【讨论】:

以上是关于从 XML 文档创建的 Json 反序列化到 POCO 不适用于数组的主要内容,如果未能解决你的问题,请参考以下文章

GolangGo 语言 XML 的序列与反序列化实践

GolangGo 语言 XML 的序列与反序列化实践

GolangGo 语言 XML 的序列与反序列化实践

xmldecoder反序列化漏洞分析

Json或XML快速反序列化类(Visual Studio 2012以上)

如何反序列化 XML 到 json 和 json 到 c# 类?