127

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

在以下从SendGrid API 参考中获取的示例负载中,您会注意到category每个项目的属性可以是单个字符串或字符串数​​组。

[
  {
    "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 处理这个问题?

4

8 回答 8

249

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

在我们使用转换器之前,我们需要定义一个类来反序列化数据。对于Categories可以在单个项目和数组之间变化的属性,将其定义为 aList<string>并用 [JsonConverter]属性标记它,以便 JSON.Net 知道使用该属性的自定义转换器。我还建议使用[JsonProperty]属性,以便可以为成员属性赋予有意义的名称,而与 JSON 中定义的内容无关。

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覆盖或将其更改为 return 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

于 2013-09-25T05:50:09.647 回答
7

我为此工作了很长时间,并感谢布赖恩的回答。我要添加的只是 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)

希望这可以节省您一些时间

于 2015-09-17T11:15:55.153 回答
6

作为对Brian Rogers出色答案的一个小改动,这里有两个经过调整的.SingleOrArrayConverter<T>

首先,这是一个适用于所有本身不是集合​​的List<T>类型的版本:T

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

笔记:

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

  • 该转换器不适用于其项目也被序列化为集合的列表,例如List<string []>

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

  • 转换器ReadJson()使用existingValueif pre-allocated 以支持填充 get-only 列表成员。

其次,这是一个适用于其他通用集合的版本,例如ObservableCollection<T>

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

然后,如果您的模型正在使用例如 an ObservableCollection<T>for some T,您可以按如下方式应用它:

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类型必须是读/写的并且具有无参数的构造函数。

演示在这里摆弄基本的单元测试。

于 2018-12-14T00:29:00.967 回答
1

Just wanted to add to @dbc excellent response above on the SingleOrArrayCollectionConverter. I was able to modify it to use with a stream from an HTTP client. Here is a snippet (you will have to set up the requestUrl (string) and the httpClient (using System.Net.Http;).

public async Task<IList<T>> HttpRequest<T>(HttpClient httpClient, string requestedUrl, CancellationToken cancellationToken)
    {
       using (var request = new HttpRequestMessage(HttpMethod.Get, requestedUrl))
       using (var httpResponseMessage = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken))
       {
          if (httpResponseMessage.IsSuccessStatusCode)
          {
             using var stream = await httpResponseMessage.Content.ReadAsStreamAsync();    
             using var streamReader = new StreamReader(stream);
             using var jsonTextReader = new JsonTextReader(streamReader );
             var settings = new JsonSerializerSettings
             {
                // Pass true if you want single-item lists to be reserialized as single items
                Converters = { new SingleOrArrayCollectionConverter(true) },
             };
             var jsonSerializer = JsonSerializer.Create(settings);
             return jsonSerializer.Deserialize<List<T>>(jsonTextReader);
     }

I apologize if there are missing brackets or misspellings, it was not easy to paste code in here.

于 2021-11-19T19:32:52.793 回答
1

要处理此问题,您必须使用自定义 JsonConverter。但你可能已经想到了这一点。您只是在寻找可以立即使用的转换器。这不仅为所描述的情况提供了解决方案。我举一个问题的例子。

如何使用我的转换器:

在属性上方放置一个 JsonConverter 属性。JsonConverter(typeof(SafeCollectionConverter))

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

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

    [JsonProperty("category"), JsonConverter(typeof(SafeCollectionConverter))]
    public string[] Category { get; set; }

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

这是我的转换器:

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;

namespace stackoverflow.question18994685
{
    public class SafeCollectionConverter : JsonConverter
    {
        public override bool CanConvert(Type objectType)
        {
            return true;
        }

        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            //This not works for Populate (on existingValue)
            return serializer.Deserialize<JToken>(reader).ToObjectCollectionSafe(objectType, serializer);
        }     

        public override bool CanWrite => false;

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

此转换器使用以下类:

using System;

namespace Newtonsoft.Json.Linq
{
    public static class SafeJsonConvertExtensions
    {
        public static object ToObjectCollectionSafe(this JToken jToken, Type objectType)
        {
            return ToObjectCollectionSafe(jToken, objectType, JsonSerializer.CreateDefault());
        }

        public static object ToObjectCollectionSafe(this JToken jToken, Type objectType, JsonSerializer jsonSerializer)
        {
            var expectArray = typeof(System.Collections.IEnumerable).IsAssignableFrom(objectType);

            if (jToken is JArray jArray)
            {
                if (!expectArray)
                {
                    //to object via singel
                    if (jArray.Count == 0)
                        return JValue.CreateNull().ToObject(objectType, jsonSerializer);

                    if (jArray.Count == 1)
                        return jArray.First.ToObject(objectType, jsonSerializer);
                }
            }
            else if (expectArray)
            {
                //to object via JArray
                return new JArray(jToken).ToObject(objectType, jsonSerializer);
            }

            return jToken.ToObject(objectType, jsonSerializer);
        }

        public static T ToObjectCollectionSafe<T>(this JToken jToken)
        {
            return (T)ToObjectCollectionSafe(jToken, typeof(T));
        }

        public static T ToObjectCollectionSafe<T>(this JToken jToken, JsonSerializer jsonSerializer)
        {
            return (T)ToObjectCollectionSafe(jToken, typeof(T), jsonSerializer);
        }
    }
}

它究竟是做什么的?如果您放置转换器属性,转换器将用于此属性。如果您期望 json 数组有 1 或没有结果,则可以在普通对象上使用它。或者你IEnumerable在你期望一个 json 对象或 json 数组的地方使用它。(知道一个array- object[]- 是一个IEnumerable) 一个缺点是这个转换器只能放在一个属性之上,因为他认为他可以转换所有东西。并被警告。Astring也是一个IEnumerable

它提供的不仅仅是问题的答案:如果您通过 id 搜索某些内容,您知道您将得到一个返回一个或没有结果的数组。该ToObjectCollectionSafe<TResult>()方法可以为您处理。

这可用于使用 JSON.net 的 Single Result vs Array 并处理同一属性的单个项目和数组,并且可以将数组转换为单个对象。

我为服务器上的 REST 请求制作了这个,过滤器在数组中返回了一个结果,但希望在我的代码中将结果作为单个对象返回。也适用于具有扩展结果的 OData 结果响应,其中包含数组中的一项。

玩得开心。

于 2019-11-05T10:01:31.360 回答
0

您可以使用JSONConverterAttribute此处找到的:http: //james.newtonking.com/projects/json/help/

假设你有一个看起来像

public class RootObject
{
    public string email { get; set; }
    public int timestamp { get; set; }
    public string smtpid { get; set; }
    public string @event { get; set; }
    public string category[] { get; set; }
}

您将装饰 category 属性,如下所示:

    [JsonConverter(typeof(SendGridCategoryConverter))]
    public string category { get; set; }

public class SendGridCategoryConverter : JsonConverter
{
  public override bool CanConvert(Type objectType)
  {
    return true; // add your own logic
  }

  public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
  {
   // do work here to handle returning the array regardless of the number of objects in 
  }

  public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
  {
    // Left as an exercise to the reader :)
    throw new NotImplementedException();
  }
}
于 2013-09-25T01:30:14.760 回答
0

我有一个非常相似的问题。我的 Json 请求对我来说是完全未知的。我只知道。

其中会有一个 objectId 和一些匿名键值对和数组。

我将它用于我所做的 EAV 模型:

我的 JSON 请求:

{objectId": 2, "firstName": "Hans", "email" :[ "a@b.de","a@c.de"], "name": "Andre", "something" :[" 232","123"] }

我的班级我定义:

[JsonConverter(typeof(AnonyObjectConverter))]
public class AnonymObject
{
    public AnonymObject()
    {
        fields = new Dictionary<string, string>();
        list = new List<string>();
    }

    public string objectid { get; set; }
    public Dictionary<string, string> fields { get; set; }
    public List<string> list { get; set; }
}

现在我想反序列化未知属性及其值和数组,我的转换器看起来像这样:

   public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        AnonymObject anonym = existingValue as AnonymObject ?? new AnonymObject();
        bool isList = false;
        StringBuilder listValues = new StringBuilder();

        while (reader.Read())
        {
            if (reader.TokenType == JsonToken.EndObject) continue;

            if (isList)
            {
                while (reader.TokenType != JsonToken.EndArray)
                {
                    listValues.Append(reader.Value.ToString() + ", ");

                    reader.Read();
                }
                anonym.list.Add(listValues.ToString());
                isList = false;

                continue;
            }

            var value = reader.Value.ToString();

            switch (value.ToLower())
            {
                case "objectid":
                    anonym.objectid = reader.ReadAsString();
                    break;
                default:
                    string val;

                    reader.Read();
                    if(reader.TokenType == JsonToken.StartArray)
                    {
                        isList = true;
                        val = "ValueDummyForEAV";
                    }
                    else
                    {
                        val = reader.Value.ToString();
                    }
                    try
                    {
                        anonym.fields.Add(value, val);
                    }
                    catch(ArgumentException e)
                    {
                        throw new ArgumentException("Multiple Attribute found");
                    }
                    break;
            }

        }

        return anonym;
    }

所以现在每次我得到一个 AnonymObject 时,我都可以遍历 Dictionary 并且每次有我的标志“ValueDummyForEAV”时,我都会切换到列表,读取第一行并拆分值。之后我从列表中删除第一个条目并继续从字典中迭代。

也许有人有同样的问题,可以使用这个:)

问候安德烈

于 2016-04-20T12:56:37.017 回答
-2

我找到了另一种解决方案,可以通过使用 object 将类别处理为字符串或数组。这样我就不需要弄乱 json 序列化程序了。

如果你有时间,请给它看看,并告诉我你的想法。https://github.com/MarcelloCarreira/sendgrid-csharp-eventwebhook

它基于https://sendgrid.com/blog/tracking-email-using-azure-sendgrid-event-webhook-part-1/的解决方案,但我还添加了时间戳的日期转换,升级了变量以反映当前的 SendGrid 模型(并使类别起作用)。

我还创建了一个带有基本身份验证选项的处理程序。请参阅 ashx 文件和示例。

谢谢!

于 2015-05-15T00:56:57.017 回答