Как обрабатывать как один элемент, так и массив для одного и того же свойства с помощью JSON.net


Я пытаюсь исправить свою библиотеку SendGridPlus для работы с событиями SendGrid, но у меня возникли некоторые проблемы с непоследовательной обработкой категорий в API.

в следующем примере полезная нагрузка взята из SendGrid API reference, вы заметите, что 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?

5 63

5 ответов:

лучший способ справиться с этой ситуацией-использовать пользовательский 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

EDIT

Если вам нужно пойти другим путем, т. е. сериализовать, сохраняя тот же формат, вы можете использовать тег 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)

надеюсь, это сэкономит вам некоторое время

у меня была очень похожая проблема. Мой запрос Json был совершенно неизвестен для меня. Я только знал.

в нем будет objectId и некоторые пары и массивы значений ключей anonym.

я использовал его для модели подслушивания я сделал:

мой запрос JSON:

{objectId": 2, "firstName": "Hans", "электронная почта": ["a@b.de","a@c.de"], "имя": "Андре", "что-то" :["232","123"] }

мой класс i определено:

[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, я могу перебирать словарь, и каждый раз, когда есть мой флаг "ValueDummyForEAV", я переключаюсь на список, читаю первую строку и разделяю значения. После этого я удаляю первую запись из списка и продолжаю итерацию из словаря.

может, кто-то имеет ту же проблему и может использовать это :)

С уважением Андре

можно использовать 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; }
}

вы бы украсить свойство категории, как видно здесь:

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

Я нашел другое решение, которое может обрабатывать категорию как строку или массив с помощью объекта. Таким образом, мне не нужно связываться с сериализатором json.

пожалуйста, дайте ему посмотреть, если у вас есть время и скажите мне, что вы думаете. https://github.com/MarcelloCarreira/sendgrid-csharp-eventwebhook

Он основан на решении в https://sendgrid.com/blog/tracking-email-using-azure-sendgrid-event-webhook-part-1/ но я также добавил дату преобразование из метки времени, обновил переменные, чтобы отразить текущую модель SendGrid (и сделал категории работы).

Я также создал обработчик с базовой авторизацией в качестве опции. См. файлы ashx и примеры.

спасибо!