提问者:小点点

如何使用 JSON.net 处理同一属性的单个项目和数组


我正在尝试修复我的 SendGridPlus 库来处理 SendGrid 事件,但我在 API 中类别处理不一致时遇到了一些麻烦。

在以下示例有效负载中,取自 SendGrid API 参考,你将注意到每个项的类别属性可以是单个字符串或字符串数组。

[
  {
    "email": "john.doe@sendgrid.com",
    "timestamp": 1337966815,
    "category": [
      "newuser",
      "transactional"
    ],
    "event": "open"
  },
  {
    "email": "jane.doe@sendgrid.com",
    "timestamp": 1337966815,
    "category": "olduser",
    "event": "open"
  }
]

似乎我做出这样的 JSON.NET 的选择是在字符串进入之前修复字符串,或者配置 JSON.NET 接受不正确的数据。如果我能侥幸逃脱,我宁愿不做任何字符串解析。

还有其他方法可以使用 Json.Net 来处理这个问题吗?


共3个答案

匿名用户

处理这种情况的最佳方法是使用自定义 JsonConverter

在进入转换器之前,我们需要定义一个类来反序列化数据。对于在单个项和数组之间可能不同的 Category 属性,请将其定义为 List

class Item
{
    [JsonProperty("email")]
    public string Email { get; set; }

    [JsonProperty("timestamp")]
    public int Timestamp { get; set; }

    [JsonProperty("event")]
    public string Event { get; set; }

    [JsonProperty("category")]
    [JsonConverter(typeof(SingleOrArrayConverter<string>))]
    public List<string> Categories { get; set; }
}

以下是我将如何实现转换器。请注意,我已将转换器设置为通用转换器,以便可以根据需要将其与字符串或其他类型的对象一起使用。

class SingleOrArrayConverter<T> : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return (objectType == typeof(List<T>));
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        JToken token = JToken.Load(reader);
        if (token.Type == JTokenType.Array)
        {
            return token.ToObject<List<T>>();
        }
        return new List<T> { token.ToObject<T>() };
    }

    public override bool CanWrite
    {
        get { return false; }
    }

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

下面是一个简短的程序,演示了转换器在示例数据中的运行情况:

class Program
{
    static void Main(string[] args)
    {
        string json = @"
        [
          {
            ""email"": ""john.doe@sendgrid.com"",
            ""timestamp"": 1337966815,
            ""category"": [
              ""newuser"",
              ""transactional""
            ],
            ""event"": ""open""
          },
          {
            ""email"": ""jane.doe@sendgrid.com"",
            ""timestamp"": 1337966815,
            ""category"": ""olduser"",
            ""event"": ""open""
          }
        ]";

        List<Item> list = JsonConvert.DeserializeObject<List<Item>>(json);

        foreach (Item obj in list)
        {
            Console.WriteLine("email: " + obj.Email);
            Console.WriteLine("timestamp: " + obj.Timestamp);
            Console.WriteLine("event: " + obj.Event);
            Console.WriteLine("categories: " + string.Join(", ", obj.Categories));
            Console.WriteLine();
        }
    }
}

最后,这是上述内容的输出:

email: john.doe@sendgrid.com
timestamp: 1337966815
event: open
categories: newuser, transactional

email: jane.doe@sendgrid.com
timestamp: 1337966815
event: open
categories: olduser

小提琴:https://dotnetfiddle.net/lERrmu

编辑

如果你需要走另一条路,即序列化,同时保持相同的格式,你可以实现转换器的 WriteJson() 方法,如下所示。(请务必删除 CanWrite 覆盖或将其更改为返回 true,否则将永远不会调用 WriteJson()。

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        List<T> list = (List<T>)value;
        if (list.Count == 1)
        {
            value = list[0];
        }
        serializer.Serialize(writer, value);
    }

小提琴:https://dotnetfiddle.net/XG3eRy

匿名用户

我为此工作了很长时间,感谢布莱恩的回答。我添加的只是 vb.net 答案!

Public Class SingleValueArrayConverter(Of T)
sometimes-array-and-sometimes-object
    Inherits JsonConverter
    Public Overrides Sub WriteJson(writer As JsonWriter, value As Object, serializer As JsonSerializer)
        Throw New NotImplementedException()
    End Sub

    Public Overrides Function ReadJson(reader As JsonReader, objectType As Type, existingValue As Object, serializer As JsonSerializer) As Object
        Dim retVal As Object = New [Object]()
        If reader.TokenType = JsonToken.StartObject Then
            Dim instance As T = DirectCast(serializer.Deserialize(reader, GetType(T)), T)
            retVal = New List(Of T)() From { _
                instance _
            }
        ElseIf reader.TokenType = JsonToken.StartArray Then
            retVal = serializer.Deserialize(reader, objectType)
        End If
        Return retVal
    End Function
    Public Overrides Function CanConvert(objectType As Type) As Boolean
        Return False
    End Function
End Class

然后在您的班级中:

 <JsonProperty(PropertyName:="JsonName)> _
 <JsonConverter(GetType(SingleValueArrayConverter(Of YourObject)))> _
    Public Property YourLocalName As List(Of YourObject)

希望这能为您节省一些时间

匿名用户

作为Brian Rogers伟大答案的一个小变化,这里有两个调整版本的SingleOrArrayConverter。

首先,这是一个适用于所有列表的版本

public class SingleOrArrayListConverter : JsonConverter
{
    // Adapted from this answer https://stackoverflow.com/a/18997172
    // to https://stackoverflow.com/questions/18994685/how-to-handle-both-a-single-item-and-an-array-for-the-same-property-using-json-n
    // by Brian Rogers https://stackoverflow.com/users/10263/brian-rogers
    readonly bool canWrite;
    readonly IContractResolver resolver;

    public SingleOrArrayListConverter() : this(false) { }

    public SingleOrArrayListConverter(bool canWrite) : this(canWrite, null) { }

    public SingleOrArrayListConverter(bool canWrite, IContractResolver resolver)
    {
        this.canWrite = canWrite;
        // 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));
        if (reader.MoveToContent().TokenType == JsonToken.Null)
            return null;
        var list = (IList)(existingValue ?? contract.DefaultCreator());
        if (reader.TokenType == JsonToken.StartArray)
            serializer.Populate(reader, list);
        else
            // Here we take advantage of the fact that List<T> implements IList to avoid having to use reflection to call the generic Add<T> method.
            list.Add(serializer.Deserialize(reader, itemType));
        return list;
    }

    public override bool CanWrite { get { return canWrite; } }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        var list = value as ICollection;
        if (list == null)
            throw new JsonSerializationException(string.Format("Invalid type for {0}: {1}", GetType(), value.GetType()));
        // Here we take advantage of the fact that List<T> implements IList to avoid having to use reflection to call the generic Count method.
        if (list.Count == 1)
        {
            foreach (var item in list)
            {
                serializer.Serialize(writer, item);
                break;
            }
        }
        else
        {
            writer.WriteStartArray();
            foreach (var item in list)
                serializer.Serialize(writer, item);
            writer.WriteEndArray();
        }
    }
}

public static partial class JsonExtensions
{
    public static JsonReader MoveToContent(this JsonReader reader)
    {
        while ((reader.TokenType == JsonToken.Comment || reader.TokenType == JsonToken.None) && reader.Read())
            ;
        return reader;
    }

    internal 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 settings = new JsonSerializerSettings
{
    // Pass true if you want single-item lists to be reserialized as single items
    Converters = { new SingleOrArrayListConverter(true) },
};
var list = JsonConvert.DeserializeObject<List<Item>>(json, settings);

笔记:

>

  • 转换器避免了将整个 JSON 值作为 JToken 层次结构预加载到内存中的需要。

    转换器不适用于其项目也被序列化为集合的列表,例如 List

    传递给构造函数的布尔 canWrite 参数控制是将单元素列表重新序列化为 JSON 值还是 JSON 数组。

    转换器的 ReadJson() 使用现有的值(如果预先分配),以支持填充仅获取列表成员。

    其次,这是一个适用于其他泛型集合(如 ObservableCollection)的版本

    public class SingleOrArrayCollectionConverter<TCollection, TItem> : JsonConverter
        where TCollection : ICollection<TItem>
    {
        // Adapted from this answer https://stackoverflow.com/a/18997172
        // to https://stackoverflow.com/questions/18994685/how-to-handle-both-a-single-item-and-an-array-for-the-same-property-using-json-n
        // by Brian Rogers https://stackoverflow.com/users/10263/brian-rogers
        readonly bool canWrite;
    
        public SingleOrArrayCollectionConverter() : this(false) { }
    
        public SingleOrArrayCollectionConverter(bool canWrite) { this.canWrite = canWrite; }
    
        public override bool CanConvert(Type objectType)
        {
            return typeof(TCollection).IsAssignableFrom(objectType);
        }
    
        static void ValidateItemContract(IContractResolver resolver)
        {
            var itemContract = resolver.ResolveContract(typeof(TItem));
            if (itemContract is JsonArrayContract)
                throw new JsonSerializationException(string.Format("Item contract type {0} not supported.", itemContract));
        }
    
        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            ValidateItemContract(serializer.ContractResolver);
            if (reader.MoveToContent().TokenType == JsonToken.Null)
                return null;
            var list = (ICollection<TItem>)(existingValue ?? serializer.ContractResolver.ResolveContract(objectType).DefaultCreator());
            if (reader.TokenType == JsonToken.StartArray)
                serializer.Populate(reader, list);
            else
                list.Add(serializer.Deserialize<TItem>(reader));
            return list;
        }
    
        public override bool CanWrite { get { return canWrite; } }
    
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            ValidateItemContract(serializer.ContractResolver);
            var list = value as ICollection<TItem>;
            if (list == null)
                throw new JsonSerializationException(string.Format("Invalid type for {0}: {1}", GetType(), value.GetType()));
            if (list.Count == 1)
            {
                foreach (var item in list)
                {
                    serializer.Serialize(writer, item);
                    break;
                }
            }
            else
            {
                writer.WriteStartArray();
                foreach (var item in list)
                    serializer.Serialize(writer, item);
                writer.WriteEndArray();
            }
        }
    }
    

    然后,如果您的模型正在使用可观察集合

    class Item
    {
        public string Email { get; set; }
        public int Timestamp { get; set; }
        public string Event { get; set; }
    
        [JsonConverter(typeof(SingleOrArrayCollectionConverter<ObservableCollection<string>, string>))]
        public ObservableCollection<string> Category { get; set; }
    }
    

    笔记:

    • 除了 SingleOrArrayListConverter 的注释和限制之外,TCollection 类型必须是可读/写的,并且具有无参数构造函数。

    在此处演示基本单元测试。