144 votes

Comment gérer à la fois un seul élément et un tableau pour la même propriété en utilisant JSON.net

Je suis en train de corriger ma bibliothèque SendGridPlus pour gérer les événements SendGrid, mais j'ai quelques problèmes avec le traitement incohérent des catégories dans l'API.

Dans l'exemple de charge utile suivant tiré de la référence de l'API SendGrid, vous remarquerez que la propriété category pour chaque élément peut être soit une chaîne unique, soit un tableau de chaînes.

[
  {
    "email": "john.doe@sendgrid.com",
    "timestamp": 1337966815,
    "category": [
      "newuser",
      "transactional"
    ],
    "event": "open"
  },
  {
    "email": "jane.doe@sendgrid.com",
    "timestamp": 1337966815,
    "category": "olduser",
    "event": "open"
  }
]

Il semble que mes options pour faire fonctionner JSON.NET de cette manière sont de corriger la chaîne avant son traitement, ou de configurer JSON.NET pour accepter les données incorrectes. Je préférerais éviter tout traitement de chaîne si possible.

Y a-t-il un autre moyen de gérer cela en utilisant Json.Net ?

277voto

Brian Rogers Points 12160

La meilleure façon de gérer cette situation est d'utiliser un JsonConverter personnalisé.

Avant d'arriver au convertisseur, nous devrons définir une classe pour désérialiser les données. Pour la propriété Categories qui peut varier entre un seul élément et un tableau, définissez-la comme une List et marquez-la avec un attribut [JsonConverter] afin que JSON.Net sache d'utiliser le convertisseur personnalisé pour cette propriété. Je recommande également d'utiliser des attributs [JsonProperty] pour que les propriétés membres puissent avoir des noms significatifs indépendamment de ce qui est défini dans le 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))]
    public List Categories { get; set; }
}

Voici comment j'implémenterais le convertisseur. Remarquez que j'ai rendu le convertisseur générique afin qu'il puisse être utilisé avec des chaînes de caractères ou d'autres types d'objets selon les besoins.

class SingleOrArrayConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return (objectType == typeof(List));
    }

    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>();
        }
        if (token.Type == JTokenType.Null)
        {
            return null;
        }
        return new List { token.ToObject() };
    }

    public override bool CanWrite
    {
        get { return false; }
    }

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

Voici un petit programme démontrant le convertisseur en action avec vos données d'exemple :

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 list = JsonConvert.DeserializeObject>(json);

        foreach (Item obj in list)
        {
            Console.WriteLine("email: " + obj.Email);
            Console.WriteLine("timestamp: " + obj.Timestamp);
            Console.WriteLine("event: " + obj.Event);
            Console.WriteLine("catégories: " + string.Join(", ", obj.Categories));
            Console.WriteLine();
        }
    }
}

Et enfin, voici le résultat de ce qui précède :

email: john.doe@sendgrid.com
timestamp: 1337966815
event: open
catégories: newuser, transactional

email: jane.doe@sendgrid.com
timestamp: 1337966815
event: open
catégories: olduser

Fiddle : https://dotnetfiddle.net/lERrmu

MODIFICATION

Si vous avez besoin d'aller dans l'autre sens, c'est-à-dire de sérialiser, tout en conservant le même format, vous pouvez implémenter la méthode WriteJson() du convertisseur comme indiqué ci-dessous. (Assurez-vous de supprimer la surcharge de CanWrite ou de la modifier pour retourner true, sinon WriteJson() ne sera jamais appelé.)

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        List list = (List)value;
        if (list.Count == 1)
        {
            value = list[0];
        }
        serializer.Serialize(writer, value);
    }

Fiddle : https://dotnetfiddle.net/XG3eRy

8voto

grantay Points 81

J'ai travaillé sur ça pendant des siècles, et merci à Brian pour sa réponse. Tout ce que j'ajoute, c'est la réponse en vb.net !:

Public Class SingleValueArrayConverter(Of T)
parfois-tableau-et-parfois-objet
    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

ensuite dans votre classe:

  _
  _
    Public Property VotreNomLocal As List(Of VotreObjet)

J'espère que cela vous fait gagner du temps

1voto

Roberto B Points 1

Pour gérer cela, vous devez utiliser un JsonConverter personnalisé. Mais vous aviez probablement déjà cela à l'esprit. Vous cherchez juste un convertisseur que vous pouvez utiliser immédiatement. Et cela offre plus qu'une simple solution pour la situation décrite. Je donne un exemple avec la question posée.

Comment utiliser mon convertisseur :

Placez un attribut JsonConverter au-dessus de la propriété. 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; }
}

Et voici mon convertisseur :

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)
        {
            // Cela ne fonctionne pas pour Populer (sur existingValue)
            return serializer.Deserialize(reader).ToObjectCollectionSafe(objectType, serializer);
        }     

        public override bool CanWrite => false;

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

Et ce convertisseur utilise la classe suivante :

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(this JToken jToken)
        {
            return (T)ToObjectCollectionSafe(jToken, typeof(T));
        }

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

Que fait-il exactement ? Si vous placez l'attribut du convertisseur, le convertisseur sera utilisé pour cette propriété. Vous pouvez l'utiliser sur un objet normal si vous vous attendez à un tableau json avec 1 ou aucun résultat. Ou vous l'utilisez sur un IEnumerable où vous vous attendez à un objet json ou à un tableau json. (Sachez qu'un array - un object[] - est un IEnumerable). Un inconvénient est que ce convertisseur ne peut être placé que au-dessus d'une propriété parce qu'il pense qu'il peut tout convertir. Et soyez averti. Une string est aussi un IEnumerable.

Et cela offre plus qu'une réponse à la question : Si vous cherchez quelque chose par id, vous savez que vous obtiendrez un tableau avec un ou aucun résultat. La méthode ToObjectCollectionSafe() peut s'en charger pour vous.

Ceci est utilisable pour un résultat unique vs un tableau en utilisant JSON.net et gérer à la fois un élément unique et un tableau pour la même propriété et peut convertir un tableau en un objet unique.

J'ai fait cela pour les requêtes REST sur un serveur avec un filtre qui retournait un résultat dans un tableau mais voulait obtenir le résultat sous forme d'un objet unique dans mon code. Et aussi pour une réponse de résultat OData avec un résultat étendu avec un seul élément dans un tableau.

Amusez-vous avec cela.

1voto

Fradsham Points 51

Voulais simplement ajouter à la excellente réponse de @dbc ci-dessus sur le SingleOrArrayCollectionConverter. J'ai pu le modifier pour l'utiliser avec un flux provenant d'un client HTTP. Voici un extrait (vous devrez configurer l'URL de la requête (chaîne) et le httpClient (en utilisant System.Net.Http;).

public async Task> HttpRequest(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>(jsonTextReader);
     }

Je m'excuse s'il manque des crochets ou des fautes d'orthographe, ce n'était pas facile de coller du code ici.

1voto

LakShan Points 111

Pour ceux qui cherchent une solution en utilisant System.Text.Json

public class SingleOrArrayConverter : JsonConverter>
{
    public override List Read(
        ref Utf8JsonReader reader,
        Type typeToConvert,
        JsonSerializerOptions options)
    {
        switch (reader.TokenType)
        {
            case JsonTokenType.Null:
                return null;
            case JsonTokenType.StartArray:
                var list = new List();
                while (reader.Read())
                {
                    if (reader.TokenType == JsonTokenType.EndArray)
                        break;
                    list.Add(JsonSerializer.Deserialize(ref reader, options));
                }
                return list;
            default:
                return new List { JsonSerializer.Deserialize(ref reader, options) };
        }
    }

    public override void Write(
        Utf8JsonWriter writer,
        List objectToWrite,
        JsonSerializerOptions options) =>
        JsonSerializer.Serialize(writer, objectToWrite, objectToWrite.GetType(), options);
}

La réponse a été inspirée par la réponse de Brian Rogers et la réponse de @dbc de ici

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X