Подтвердить что ты не робот

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

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

В следующем примере полезной нагрузки, взятой из ссылки API-интерфейса SendGrid, вы заметите, что свойство category для каждого элемента может быть как отдельной строкой, так и массив строк.

[
  {
    "email": "[email protected]",
    "timestamp": 1337966815,
    "category": [
      "newuser",
      "transactional"
    ],
    "event": "open"
  },
  {
    "email": "[email protected]",
    "timestamp": 1337966815,
    "category": "olduser",
    "event": "open"
  }
]

Кажется, мои возможности сделать JSON.NET подобным образом - это исправление строки перед ее входом или настройка JSON.NET для принятия неверных данных. Я бы предпочел не выполнять синтаксический анализ строк, если мне это удастся.

Есть ли другой способ справиться с этим с помощью Json.Net?

4b9b3361

Ответ 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"": ""[email protected]"",
            ""timestamp"": 1337966815,
            ""category"": [
              ""newuser"",
              ""transactional""
            ],
            ""event"": ""open""
          },
          {
            ""email"": ""[email protected]"",
            ""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: [email protected]
timestamp: 1337966815
event: open
categories: newuser, transactional

email: [email protected]
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

Ответ 2

Я работал над этим целую вечность, и благодаря Брайану за его ответ. Все, что я добавляю, это ответ 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)

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

Ответ 3

Вы можете использовать JSONConverterAttribute как 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();
  }
}

Ответ 4

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

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

Я использовал его для модели EAV, которую я сделал:

Мой запрос JSON:

{objectId ": 2," firstName ":" Hans "," email ": [" [email protected] "," [email protected] "]," name ":" Andre "," что-то ": [" 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, я могу перебирать словарь, и каждый раз, когда появляется мой флаг "ValueDummyForEAV", я переключаюсь в список, читаю первую строку и делю значения. После этого я удаляю первую запись из списка и продолжаю итерацию из словаря.

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

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

Ответ 5

В качестве небольшого изменения великого ответа Брайана Роджерса, здесь есть две подправленные версии 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);

Заметки:

  • Преобразователь избавляет от необходимости предварительно загружать все значения JSON в память в виде иерархии JToken.

  • Конвертер не применяется к спискам, элементы которых также сериализуются как коллекции, например List<string []>

  • Логический аргумент canWrite передаваемый конструктору, управляет повторной сериализацией одноэлементных списков в виде значений JSON или массивов JSON.

  • Преобразователь ReadJson() использует existingValue если оно предварительно выделено, чтобы поддерживать ReadJson() элементов списка только для получения.

Во-вторых, вот версия, которая работает с другими универсальными коллекциями, такими как 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();
        }
    }
}

Затем, если ваша модель использует, скажем, ObservableCollection<T> для некоторого 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 должен быть TCollection для чтения/записи и иметь конструктор без параметров.

Дема скрипка с основными модульными тестами здесь.

Ответ 6

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

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

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

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

Спасибо!