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


101

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

Answers:


203

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

在进入转换器之前,我们需要定义一个类来反序列化数据。对于Categories可以在单个项目和数组之间变化List<string>[JsonConverter]属性,请将其定义为a 并用一个属性标记, 以便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


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

没问题; 很高兴您发现它很有帮助。
Brian Rogers 2013年

1
优秀的!香港专业教育学院一直在寻找。@BrianRogers,如果您曾经在阿姆斯特丹,那我在喝酒!
Mad Dog Tannen 2015年

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

1
@ShaunLangley若要使转换器使用数组而不是列表,List<T>请将转换器中的所有引用T[]更改.Count为,然后更改为.Lengthdotnetfiddle.net/vnCNgZ
Brian Rogers,

6

我已经为此工作了很长时间,并感谢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

然后在你的课上:

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

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


打字错误:<JsonConverter(GetType(SingleValueArrayConverter(Of YourObject)))> _公共属性YourLocalName作为List(Of YourObject)
GlennG 2016年

3

作为Brian Rogers很好回答的一个小变体,这里有两个调整的版本。SingleOrArrayConverter<T>

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

public class SingleOrArrayListConverter : JsonConverter
{
    // Adapted from this answer https://stackoverflow.com/a/18997172
    // to /programming/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<string []>

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

  • 转换器ReadJson()使用existingValueif预先分配,以支持填充仅获取列表成员。

其次,这是一个可与其他通用集合一起使用的版本,例如ObservableCollection<T>

public class SingleOrArrayCollectionConverter<TCollection, TItem> : JsonConverter
    where TCollection : ICollection<TItem>
{
    // Adapted from this answer https://stackoverflow.com/a/18997172
    // to /programming/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类型还必须是可读写的,并且具有无参数的构造函数。

演示在这里进行基本单元测试。


0

我有一个非常类似的问题。我的Json Request完全不为我所知。我只知道

其中将有一个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; }
}

现在我想使用其值和数组反序列化未知属性,我的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”时,我切换到列表,读取第一行并拆分值。之后,我从列表中删除第一个条目,然后继续从Dictionary进行迭代。

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

问候安德烈


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

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

+“ \”事件\“:\”已处理\“,\ n” +“} \ n” +“]”;
罗伯特·麦克劳斯

它很好地处理了第一个对象,并且无法很好地处理数组。但是,当我为第二个对象创建一个数组时,它失败了。
罗伯特·麦克劳斯

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

Brian的答案中有更多更好的细节。使用那个:)
Tim Gabrhel 2013年

0

要处理此问题,您必须使用自定义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)的缺点是,这种转换只能在上面摆放着财产,因为他认为他可以把一切。并予以警告。A string也是一个IEnumerable

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

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

我针对服务器上的REST请求使用了过滤器,该过滤器在数组中返回一个结果,但希望将结果作为单个对象返回到我的代码中。同样,对于OData结果响应,其结果具有数组中一项的扩展结果。

玩得开心。


-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文件和示例。

谢谢!

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.