.Net

System.Text.Json avec .net 5

Déc 3, 2020

Etienne Pommier

Nous allons voir ensemble les nouveautés introduites par le sérialiseur JSON avec la dernière version de .NET. Dans un premier temps nous expliquerons pourquoi ce nouveau namespace est apparu et en quoi il peut nous être bénéfique. Puis nous verrons les nouvelles fonctionnalités introduites avec ce sérialiseur à travers la création d’une WepAPI avec .NET Core 5 RC 2.

Pourquoi un autre sérialiseur JSON ?

Les développeurs de Microsoft ont invoqué trois raisons principales :

  • Les performances
  • Supprimer la dépendance à Json.NET de ASP.NET Core
  • S’intégrer facilement à ASP.NET Core

Ils ont aussi estimé que contribuer au projet Json.NET n’était pas compatible avec les fonctionnalités et les améliorations de performances qu’ils souhaitaient car cela aurait, selon eux, introduit beaucoup trop de breaking changes. En ce qui concerne les performances, le changement principal est l’utilisation de Span<T>, ce qui réduit l’impact mémoire des processus de (dé)sérialisation. L’autre avantage est la prise en charge de l’UTF-8 directement sans passer par un transcodage en UTF-16. Microsoft a annoncé un gain de performances conséquent avec des vitesses d’exécution entre 1.3 jusqu’a 5 fois plus rapide qu’avec Json.NET.

Une autre raison invoquée était que Json.NET était devenu une dépendance du framework ASP.NET avec un cycle de vie à part (car géré par une autre équipe de développeurs) et un versionning différent. L’intégration d’un sérialiseur directement dans le framework réduit les soucis de version et les éventuels upgrades à passer sur le package nuget Newtonsoft.Json.NET.

Un autre point clé dans la vision des développeurs était de pouvoir s’intégrer facilement à l’éco-système ASP.NET. Ils ont donc développé un ensemble de méthodes d’extension pour configurer le comportement du sérialiseur Json dans le startup de notre API. Le sérialiseur .Net est maintenant totalement intégré aux middlewares pipelines d’ASP.NET Core. Le plus beau dans tout ça, c’est qu’avec ces extensions nous pouvons facilement réutiliser Json.Net comme notre sérialiseur par défaut.

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers().AddNewtonsoftJson();
}

Quoi de neuf avec .net 5 ?

Après avoir un peu expliqué la philosophie derrière la création de ce nouveau sérialiseur, plongeons dans le vif du sujet en parlant des nouveautés disponibles dans cette dernière version. Ce sérialiseur est dorénavant disponible sur Xamarin iOS / Android ce qui poursuit la logique, entrepris par Microsoft avec .Net 5, à savoir une plateforme unique pour développer.

Les performances

Nous l’avons en partie déjà évoqué précédemment mais un gros focus a été fait sur les performances et cela ne s’arrête pas avec cette version. Microsoft poursuit les améliorations de performances notamment sur le traitement des collections où les benchmarks effectués par certains développeurs nous montrent des améliorations en termes de vitesse entre 1.3 et 2.3 fois plus rapide en fonction du type de collection (dé)sérialisée.

Une autre grosse amélioration de performance a été faite au niveau de la (dé)sérialisation des classes quand le sérialiseur est configuré en mode « case insensitive », qui est le mode de configuration par défaut de Json.NET et de ce sérialiseur. Ce type de désérialisation serait annoncé comme 1.75 fois plus rapide que précédemment.

Les nouveautés en pratique

Pour commencer nous allons créer notre Web API avec .NET Core 5 RC 2 en utilisant la commande suivante :

dotnet new webapi

Nous allons configurer notre sérialiseur, pour cela on va dans le fichier Startup.cs de notre API. Une des premières nouveautés est l’introduction d’un constructeur pour JsonSerializerOptions qui prend une configuration par défaut avec l’énumération JsonSerializerDefaults. Dans notre exemple, étant donné que nous utilisons ASP.Net, cela est fait automaiquement avec la méthode AddJsonOptions qui nous retourne un JsonSerializerOptions préconfiguré pour le Web.

services.AddControllers().AddJsonOptions(o =>
    {
        o.JsonSerializerOptions.Encoder = JavaScriptEncoder.Create(UnicodeRanges.Latin1Supplement, UnicodeRanges.BasicLatin);
        o.JsonSerializerOptions.IncludeFields = true;
        o.JsonSerializerOptions.PropertyNameCaseInsensitive = true;
        o.JsonSerializerOptions.NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString;
     }
);

Ce que nous faisons ici, c’est que nous spécifions comment notre sérialiseur se comportera à travers toute notre API, et nous utilisons une des nouveautés qui est la possibilité de (dé)sérialiser les champs. Par défaut, cette option est bien évidemment désactivée. Vous remarquerez aussi la possibilité d’autoriser la lecture d’entier depuis des chaînes de caractères lors de la dé-sérialisation. Cela pourrait être utile lors de l’interfaçage avec des langages non typés tel JavaScript. Nous décidons aussi de gérer les noms de propriétés en ignorant la casse (c’est le comportement par défaut du sérialiseur JSON.net au contraire de celui-ci).

Pour aller plus loin dans notre exemple nous allons définir une API qui va nous retourner un objet SpaceData avec des informations sur les missions d’exploration dans l’espace. Nous passons outre la définition des routes pour le moment pour se concentrer sur les classes et leur attributs JSON.

public class Astros
{
    [JsonInclude]
    public string Craft { get; private set; }

    [JsonInclude]
    public string Name { get; private set;  }

    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
    public List<string> Missions;

    [JsonIgnore(Condition = JsonIgnoreCondition.Always)]
    public string Info => $"{Name} est un cosmonaute de l'équipage {Craft}.";

    public Astros(string name, string craft) => (Name, Craft) = (name, craft);
}
public class SpaceData
{
    public int PeopleInSpace { get; }

    public Dictionary<string, Astros> Astronautes { get; } 
}

À travers ces classes nous voyons plusieurs nouveautés. La plus flagrante est le nouveau paramètre Condition dans le constructeur des attributs JsonIgnore. Il permet de choisir sous quelle conditions le champ/propriété est inclus lors de la (dé)sérialisation. Vous pouvez aussi configurer le null handling de façon globale, mais cet attribut surcharge ce comportement au cas par cas. Il est aussi possible maintenant d’inclure les propriétés avec des accesseurs interne ou privé. Dans notre cas, les champs Name et Craft sont décorés avec l’attribut JsonInclude pour être (dé)sérialisés.
Ensuite sur notre seconde classe, rien de particulier, mais pour les plus connaisseurs d’entre vous, vous remarquerez ce dictionnaire et me direz que ça n’est malheureusement pas possible de (dé)sérialiser un dictionnaire non-string avec le sérialiseur .NET… Eh bien cela est enfin supporté avec cette dernière version !

Faisons maintenant un appel pour voir le JSON de retour de notre route GET qui renvoie des informations sur les astronautes présents dans l’espace.

{
  "peopleInSpace": "3",
  "astronautes": {
    "0_SER": {
      "craft": "ISS",
      "name": "Sergey Ryzhikov"
    },
    "1_KAT": {
      "craft": "ISS",
      "name": "Kate Rubins"
    },
    "2_SER": {
      "craft": "ISS",
      "name": "Sergey Kud-Sverchkov",
      "missions": [
        "Soyuz MS-17"
      ]
    }
  }
}

On voit bien ici l’effet de notre inclusion conditionnelle du champ mission qui est sérialisé uniquement si cette liste est non-nulle. Notre dictionnaire d’objets est enfin désérialisé c’était une grosse limitation du sérialiseur .NET. On remarquera au passage, que notre propriété Info n’est pas incluse comme spécifié avec l’attribut. Si vous vous rappelez dans notre Startup.cs de notre API, nous avons configuré le sérialiseur pour gérer les nombres comme des chaînes de caractères c’est pourquoi notre propriété PeopleInSpace apparait comme une chaîne dans le JSON de retour.

Nous allons maintenant regarder de plus près comment sont récupérées ces informations. Nous utilisons pour cela une API publique, qui nous retourne les personnes dans l’espace.

[HttpGet("people")]
public async Task<IActionResult> GetAsync()
{
       var client = new HttpClient() { BaseAddress = new Uri("https://api.open-notify.org") };

       var peopleInSpace = await GetUserAsync<ApiResponse>(client, "astros.json");
       var data = new SpaceData(peopleInSpace.People, peopleInSpace.People.Count);
       return new OkObjectResult(data);
}

static Task<T> GetUserAsync<T>(HttpClient client, string path) => client.GetFromJsonAsync<T>(path);

Ce qui saute aux yeux déjà c’est le nombre de lignes. J’ai tout le code utile à la récupération des personnes dans l’espace. Si j’avais voulu faire cela en .NET Core 3.1 classique j’aurais dû gérer la réponse moi-même :
Vérifier que mon code de retour est correct et ensuite appeler manuellement mon sérialiseur pour convertir notre réponse HTTP en objets.
Maintenant cela n’est plus nécessaire grâce aux méthodes d’extensions du HttpClient qui ont été ajoutées.

  • GetFromJsonAsync que nous venons de découvrir ici
  • PostAsJsonAsync qui envoie une requête POST à l’URI spécifiée contenant la valeur sérialisée en JSON dans le body de la requête.
  • PutAsJsonAsync identique mais le verbe PUT.

Ça nous évite d’écrire ces fameuses lignes de gestion de retour d’un appel à une API que nous avons tous écrit un million de fois. Maintenant c’est fini ! Ces méthodes sont dans un namespace à part System.Net.Http.Json.

En résumé

À travers cet exemple nous avons parcouru ensemble les principales nouveautés introduites par la version 5 de .NET au sérialiseur JSON et comment nous pouvons les utiliser dans nos futures Web API avec .NET 5. Le sérialiseur System.Text.Json se met au niveau des fonctionnalités de Json.NET tout en continuant son amélioration sur le plan des performances. Il se pose dorénavant comme une vraie alternative complète à Json.NET.

Voici une liste succinte des principales nouveautés de System.Text.Json :

  • Améliorations de performances : (dé)sérialisation des types collections, des chaines JSON longues, des POCOs
  • Support de la (dé)sérialisation des nombres entiers en chaînes de caractères
  • Support de la (dé)sérialisation des champs
  • Support de la (dé)sérialisation des Records (nouveau type C#)
  • Support de la (dé)sérialisation des dictionnaires de type autre que string
  • Possibilité d’ignorer conditionnellement des propriétés
  • Support de la (dé)sérialisation pour les objets avec des constructeurs contenant des paramètres
  • Méthodes d’extension pour le client HTTP pour faciliter la (dé)sérialisation JSON
  • Support des plateformes Xamarin iOS/Android

Pour la liste exhaustive des améliorations vous pouvez vous référer à l’epic suivante sur GitHub, qui recense toutes les PR qui ont été incluses dans la version 5.0 du sérialiseur System.Text.Json.

0 commentaires

Soumettre un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Découvrez nos autres articles

Aller au contenu principal