我正在尝试修复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处理吗?

#1 楼

解决这种情况的最佳方法是使用自定义JsonConverter。在转到转换器之前,我们需要定义一个类以对数据进行反序列化。对于可能在单个项目和数组之间变化的Categories属性,请将其定义为List<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替代项或将其更改为返回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

评论


完善!你是男人幸运的是,我已经完成了使用JsonProperty使属性更有意义的所有其他工作。多谢您提供完整的答案。 :)

–罗伯特·麦克劳斯
2013年9月25日14:18在

没问题;很高兴您发现它很有帮助。

–布莱恩·罗杰斯(Brian Rogers)
2013年9月25日下午14:50

优秀的!香港专业教育学院一直在寻找。 @BrianRogers,如果你去过阿姆斯特丹,我身上都会有饮料!

–疯狗Tannen
2015年4月9日在13:31



@israelaltar如果在类的list属性上使用[JsonConverter]属性,则无需将转换器添加到DeserializeObject调用中,如上面的答案所示。如果不使用该属性,那么是的,您需要将转换器传递给DeserializeObject。

–布莱恩·罗杰斯(Brian Rogers)
17年7月3日在13:44

@ShaunLangley若要使转换器使用数组而不是列表,请将转换器中对List 的所有引用更改为T [],并将.Count更改为.Length。 dotnetfiddle.net/vnCNgZ

–布莱恩·罗杰斯(Brian Rogers)
19-10-7在14:55



#2 楼

我一直在研究这个问题,感谢Brian的回答。
我要添加的就是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
 


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

评论


拼写错误: _公共属性YourLocalName作为List(Of YourObject)

– GlennG
16-3-11在15:33



#3 楼

作为Brian Rogers给出的一个很好的答案的一个较小的变体,这是SingleOrArrayConverter<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);


注意:


转换器避免将整个JSON值作为List<T>层次结构预加载到内存中。
转换器不适用于其项目也被序列化为集合的列表,例如T
传递给构造函数的布尔值JToken控制是否将单元素列表重新序列化为JSON值或JSON数组。
如果预先分配了转换器的List<string []>,则使用canWrite以便支持填充其次,这是与其他通用集合(例如ReadJson())一起使用的版本:

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


然后,如果您的模型正在使用某个existingValue,例如,您可以按以下方式应用它:

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


注意:


除了ObservableCollection<T>的注意事项和限制外,ObservableCollection<T>类型还必须是可读写的,并且必须具有无参数的构造函数。

此处提供基本的单元测试示例。

#4 楼

我遇到了非常相似的问题。
我对Json Request完全不了解。
我只知道。

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

我将其用于EAV模型:

我的JSON请求:


{objectId“:2,
“ firstName”:“汉斯”,
“ email”:[“ a@b.de”,“ a@c.de”],
“ name”:“安德烈”,
“ 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; }
}
 


,现在我想使用其值和数组反序列化未知属性,我的Converter看起来像这样:

    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时,我都可以遍历字典,并且每次都有我的Flag“ ValueDummyForEAV “我切换到列表,读取第一行并拆分值。此后,我从列表中删除第一项并继续

也许有人遇到相同的问题,可以使用此方法:)

问候
Andre

#5 楼

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

假设您有一个看起来像

< pre class =“ lang-cs prettyprint-override”> 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();
  }
}
 


评论


感谢您这样做,但是仍然不能解决问题。当一个实际数组进入时,它甚至会在我的代码甚至无法对具有实际数组的对象执行之前引发错误。 '其他信息:反序列化对象:字符串时出现意外令牌。路径'[2] .category [0]',第17行,位置27。

–罗伯特·麦克劳斯
2013年9月25日在3:32



私有字符串有效负载=“ [\ n” +“ {\ n” +“ \”电子邮件\“:\” john.doe@sendgrid.com \“,\ n” +“ \”时间戳\“:1337966815,\ n “ +” \“ smtp-id \”:\“ <4FBFC0DD.5040601@sendgrid.com> \”,\ n“ +” \“类别\”:\“ newuser \”,\ n“ +” \“事件\“:\”单击\“ \ n” +“},” +“ {” +“ \”电子邮件\“:\” john.doe@sendgrid.com \“,\ n” +“ \”时间戳\“ :1337969592,\ n“ +” \“ smtp-id \”:\“ <20120525181309.C1A9B40405B3@Example-Mac.local> \”,\ n“ +” \“类别\”:[\“ somestring1 \”, \“ somestring2 \”],\ n“ +” \“事件\”:\“已处理\”,\ n“ +”} \ n“ +”]“;

–罗伯特·麦克劳斯
2013年9月25日,下午3:34

它很好地处理了第一个对象,并且没有处理任何数组。但是,当我为第二个对象创建一个数组时,它失败了。

–罗伯特·麦克劳斯
2013年9月25日下午3:35

@AdvancedREI在看不到您的代码的情况下,我猜想您在读取JSON之后将读者放置在错误的位置。与其尝试直接使用阅读器,不如从阅读器加载JToken对象然后从那里去。请参阅我的答案以获取转换器的有效实现。

–布莱恩·罗杰斯(Brian Rogers)
2013年9月25日在12:05

Brian的答案中有更多更好的细节。用那个:)

–蒂姆·加布里埃尔
2013年9月25日13:26

#6 楼

要处理此问题,您必须使用自定义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或没有结果,则可以在普通对象上使用它。或者您在期望json对象或json数组的IEnumerable上使用它。 (知道array-object[]IEnumerable
缺点是此转换器只能放在属性上方,因为他认为自己可以转换所有内容。并予以警告。 string也是IEnumerable

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

可用于使用JSON.net的Single Result vs Array
,并为同一属性处理单个项目和数组
,可以将一个数组转换为单个对象。 。此外,对于具有扩展结果的OData结果响应,该结果具有数组中的一项。

玩得开心。

#7 楼

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

如果有时间请看一下,告诉我您的想法。 https://github.com/MarcelloCarreira/sendgrid-csharp-eventwebhook

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

我还创建了一个具有基本auth的处理程序选项。请参阅ashx文件和示例。

谢谢!