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

Deserialization to POCO from Json created of XML document does not work with arrays

我有以下 C# class

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

被序列化为 XML 文档 DataContractSerializer:

<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) returns 以下 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 属性 and/or 异常

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

你可以写一个custom JsonConverter to deserialize JSON transcoded from XML generated by DataContractSerializer for some List<T>, but it will be quite elaborate. Taking into account the "Conversion Rules" specified in the Newtonsoft documentation Converting between JSON and XML:

Because multiple nodes with the same name at the same level are grouped together into an array, the conversion process can produce different JSON depending on the number of nodes.

必须处理以下四种情况:

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

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

    或(没有命名空间):

      {
        "int": [
          "1",
          "2",
          "3"
        ]
      }
    
  2. 具有单个值的列表如下所示:

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

    或(没有命名空间):

      {
        "int": "1"
      }
    
  3. 空列表如下所示:

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

    或(无命名空间):

      null
    

    (没错,这就是<Numbers />的正确转码。)

  4. null 列表如下所示:

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

      {
        "@i:nil": "true"
      }
    

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

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-instance}nil="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<T>,其中 TIEnumerable 其他字符串,例如 List<List<int>>List<int []>).

    要实现锯齿状列表,需要将 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

演示 fiddle 此处:https://dotnetfiddle.net/9B7Sdn