Sérialisation JSON et XML dans API Web ASP.NET

Cet article décrit les formateurs JSON et XML dans API Web ASP.NET.

Dans API Web ASP.NET, un formateur de type média est un objet qui peut :

  • Lire des objets CLR à partir d’un corps de message HTTP
  • Écrire des objets CLR dans un corps de message HTTP

L’API web fournit des formateurs de type média pour JSON et XML. Le framework insère ces formateurs dans le pipeline par défaut. Les clients peuvent demander json ou XML dans l’en-tête Accepter de la requête HTTP.

Contenu

Formateur Media-Type JSON

La mise en forme JSON est fournie par la classe JsonMediaTypeFormatter . Par défaut, JsonMediaTypeFormatter utilise la bibliothèque Json.NET pour effectuer la sérialisation. Json.NET est un projet de open source tiers.

Si vous préférez, vous pouvez configurer la classe JsonMediaTypeFormatter pour utiliser dataContractJsonSerializer au lieu de Json.NET. Pour ce faire, définissez la propriété UseDataContractJsonSerializer sur true :

var json = GlobalConfiguration.Configuration.Formatters.JsonFormatter;
json.UseDataContractJsonSerializer = true;

Sérialisation JSON

Cette section décrit certains comportements spécifiques du formateur JSON, en utilisant le sérialiseur Json.NET par défaut. Il ne s’agit pas d’une documentation complète de la bibliothèque Json.NET; Pour plus d’informations, consultez la documentation Json.NET.

Qu’est-ce qui est sérialisé ?

Par défaut, toutes les propriétés et champs publics sont inclus dans le json sérialisé. Pour omettre une propriété ou un champ, décorez-le avec l’attribut JsonIgnore .

public class Product
{
    public string Name { get; set; }
    public decimal Price { get; set; }
    [JsonIgnore]
    public int ProductCode { get; set; } // omitted
}

Si vous préférez une approche « opt-in », décorez la classe avec l’attribut DataContract . Si cet attribut est présent, les membres sont ignorés, sauf s’ils ont le DataMember. Vous pouvez également utiliser DataMember pour sérialiser des membres privés.

[DataContract]
public class Product
{
    [DataMember]
    public string Name { get; set; }
    [DataMember]
    public decimal Price { get; set; }
    public int ProductCode { get; set; }  // omitted by default
}

propriétés Read-Only

Les propriétés en lecture seule sont sérialisées par défaut.

Dates

Par défaut, Json.NET écrit des dates au format ISO 8601 . Les dates utc (temps universel coordonné) sont écrites avec le suffixe « Z ». Les dates en heure locale incluent un décalage de fuseau horaire. Par exemple :

2012-07-27T18:51:45.53403Z         // UTC
2012-07-27T11:51:45.53403-07:00    // Local

Par défaut, Json.NET conserve le fuseau horaire. Vous pouvez remplacer ce paramètre en définissant la propriété DateTimeZoneHandling :

// Convert all dates to UTC
var json = GlobalConfiguration.Configuration.Formatters.JsonFormatter;
json.SerializerSettings.DateTimeZoneHandling = Newtonsoft.Json.DateTimeZoneHandling.Utc;

Si vous préférez utiliser le format de date Microsoft JSON ("\/Date(ticks)\/") au lieu de ISO 8601, définissez la propriété DateFormatHandling sur les paramètres de sérialiseur :

var json = GlobalConfiguration.Configuration.Formatters.JsonFormatter;
json.SerializerSettings.DateFormatHandling 
= Newtonsoft.Json.DateFormatHandling.MicrosoftDateFormat;

Mise en retrait

Pour écrire un json mis en retrait, définissez le paramètre Mise en forme sur Formatting.Indented :

var json = GlobalConfiguration.Configuration.Formatters.JsonFormatter;
json.SerializerSettings.Formatting = Newtonsoft.Json.Formatting.Indented;

Camel Casing

Pour écrire des noms de propriétés JSON avec une casse mixte, sans modifier votre modèle de données, définissez CamelCasePropertyNamesContractResolver sur le sérialiseur :

var json = GlobalConfiguration.Configuration.Formatters.JsonFormatter;
json.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();

Objets anonymes et Weakly-Typed

Une méthode d’action peut retourner un objet anonyme et le sérialiser au format JSON. Par exemple :

public object Get()
{
    return new { 
        Name = "Alice", 
        Age = 23, 
        Pets = new List<string> { "Fido", "Polly", "Spot" } 
    };
}

Le corps du message de réponse contient le code JSON suivant :

{"Name":"Alice","Age":23,"Pets":["Fido","Polly","Spot"]}

Si votre API web reçoit des objets JSON faiblement structurés de clients, vous pouvez désérialiser le corps de la demande en un type Newtonsoft.Json.Linq.JObject .

public void Post(JObject person)
{
    string name = person["Name"].ToString();
    int age = person["Age"].ToObject<int>();
}

Toutefois, il est généralement préférable d’utiliser des objets de données fortement typés. Vous n’avez pas besoin d’analyser les données vous-même et vous bénéficiez des avantages de la validation de modèle.

Le sérialiseur XML ne prend pas en charge les types anonymes ou les instances JObject . Si vous utilisez ces fonctionnalités pour vos données JSON, vous devez supprimer le formateur XML du pipeline, comme décrit plus loin dans cet article.

Formateur Media-Type XML

La mise en forme XML est fournie par la classe XmlMediaTypeFormatter . Par défaut, XmlMediaTypeFormatter utilise la classe DataContractSerializer pour effectuer la sérialisation.

Si vous préférez, vous pouvez configurer XmlMediaTypeFormatter pour utiliser xmlSerializer au lieu de DataContractSerializer. Pour ce faire, définissez la propriété UseXmlSerializer sur true :

var xml = GlobalConfiguration.Configuration.Formatters.XmlFormatter;
xml.UseXmlSerializer = true;

La classe XmlSerializer prend en charge un ensemble plus étroit de types que DataContractSerializer, mais donne plus de contrôle sur le code XML résultant. Envisagez d’utiliser XmlSerializer si vous devez faire correspondre un schéma XML existant.

Sérialisation XML

Cette section décrit certains comportements spécifiques du formateur XML, à l’aide du DataContractSerializer par défaut.

Par défaut, dataContractSerializer se comporte comme suit :

  • Toutes les propriétés et champs publics en lecture/écriture sont sérialisés. Pour omettre une propriété ou un champ, décorez-le avec l’attribut IgnoreDataMember .
  • Les membres privés et protégés ne sont pas sérialisés.
  • Les propriétés en lecture seule ne sont pas sérialisées. (Toutefois, le contenu d’une propriété de collection en lecture seule est sérialisé.)
  • Les noms de classe et de membre sont écrits en XML exactement comme ils apparaissent dans la déclaration de classe.
  • Un espace de noms XML par défaut est utilisé.

Si vous avez besoin de davantage de contrôle sur la sérialisation, vous pouvez décorer la classe avec l’attribut DataContract . Lorsque cet attribut est présent, la classe est sérialisée comme suit :

  • Approche « Accepter » : les propriétés et les champs ne sont pas sérialisés par défaut. Pour sérialiser une propriété ou un champ, décorez-le avec l’attribut DataMember .
  • Pour sérialiser un membre privé ou protégé, décorez-le avec l’attribut DataMember .
  • Les propriétés en lecture seule ne sont pas sérialisées.
  • Pour modifier la façon dont le nom de la classe apparaît dans le xml, définissez le paramètre Name dans l’attribut DataContract .
  • Pour modifier l’affichage d’un nom de membre dans le code XML, définissez le paramètre Name dans l’attribut DataMember .
  • Pour modifier l’espace de noms XML, définissez le paramètre Namespace dans la classe DataContract .

propriétés Read-Only

Les propriétés en lecture seule ne sont pas sérialisées. Si une propriété en lecture seule a un champ privé de stockage, vous pouvez marquer le champ privé avec l’attribut DataMember . Cette approche nécessite l’attribut DataContract sur la classe .

[DataContract]
public class Product
{
    [DataMember]
    private int pcode;  // serialized

    // Not serialized (read-only)
    public int ProductCode { get { return pcode; } }
}

Dates

Les dates sont écrites au format ISO 8601. Par exemple, « 2012-05-23T20:21:37.9116538Z ».

Mise en retrait

Pour écrire du code XML mis en retrait, affectez à la propriété Indent la valeur true :

var xml = GlobalConfiguration.Configuration.Formatters.XmlFormatter;
xml.Indent = true;

Définition Per-Type sérialiseurs XML

Vous pouvez définir différents sérialiseurs XML pour différents types CLR. Par exemple, vous pouvez avoir un objet de données particulier qui nécessite XmlSerializer pour la compatibilité descendante. Vous pouvez utiliser XmlSerializer pour cet objet et continuer à utiliser DataContractSerializer pour d’autres types.

Pour définir un sérialiseur XML pour un type particulier, appelez SetSerializer.

var xml = GlobalConfiguration.Configuration.Formatters.XmlFormatter;
// Use XmlSerializer for instances of type "Product":
xml.SetSerializer<Product>(new XmlSerializer(typeof(Product)));

Vous pouvez spécifier un XmlSerializer ou tout objet qui dérive de XmlObjectSerializer.

Suppression du formateur JSON ou XML

Vous pouvez supprimer le formateur JSON ou le formateur XML de la liste des formateurs, si vous ne souhaitez pas les utiliser. Les raisons main de procéder sont les suivantes :

  • Pour limiter vos réponses d’API web à un type de média particulier. Par exemple, vous pouvez décider de prendre en charge uniquement les réponses JSON et de supprimer le formateur XML.
  • Pour remplacer le formateur par défaut par un formateur personnalisé. Par exemple, vous pouvez remplacer le formateur JSON par votre propre implémentation personnalisée d’un formateur JSON.

Le code suivant montre comment supprimer les formateurs par défaut. Appelez-la à partir de votre méthode Application_Start , définie dans Global.asax.

void ConfigureApi(HttpConfiguration config)
{
    // Remove the JSON formatter
    config.Formatters.Remove(config.Formatters.JsonFormatter);

    // or

    // Remove the XML formatter
    config.Formatters.Remove(config.Formatters.XmlFormatter);
}

Gestion des références d’objets circulaires

Par défaut, les formateurs JSON et XML écrivent tous les objets en tant que valeurs. Si deux propriétés font référence au même objet, ou si le même objet apparaît deux fois dans une collection, le formateur sérialise l’objet deux fois. Il s’agit d’un problème particulier si votre graphique d’objets contient des cycles, car le sérialiseur lève une exception lorsqu’il détecte une boucle dans le graphe.

Considérez les modèles d’objet et le contrôleur suivants.

public class Employee
{
    public string Name { get; set; }
    public Department Department { get; set; }
}

public class Department
{
    public string Name { get; set; }
    public Employee Manager { get; set; }
}

public class DepartmentsController : ApiController
{
    public Department Get(int id)
    {
        Department sales = new Department() { Name = "Sales" };
        Employee alice = new Employee() { Name = "Alice", Department = sales };
        sales.Manager = alice;
        return sales;
    }
}

L’appel de cette action entraîne la levée d’une exception par le formateur, ce qui se traduit par une réponse status code 500 (erreur interne du serveur) au client.

Pour conserver les références d’objets dans JSON, ajoutez le code suivant à Application_Start méthode dans le fichier Global.asax :

var json = GlobalConfiguration.Configuration.Formatters.JsonFormatter;
json.SerializerSettings.PreserveReferencesHandling = 
    Newtonsoft.Json.PreserveReferencesHandling.All;

À présent, l’action du contrôleur retourne un json qui ressemble à ceci :

{"$id":"1","Name":"Sales","Manager":{"$id":"2","Name":"Alice","Department":{"$ref":"1"}}}

Notez que le sérialiseur ajoute une propriété « $id » aux deux objets. En outre, elle détecte que la propriété Employee.Department crée une boucle. Elle remplace donc la valeur par une référence d’objet : {"$ref »:"1"}.

Notes

Les références d’objet ne sont pas standard dans JSON. Avant d’utiliser cette fonctionnalité, déterminez si vos clients pourront analyser les résultats. Il peut être préférable de supprimer simplement les cycles du graphique. Par exemple, le lien entre Employé et Service n’est pas vraiment nécessaire dans cet exemple.

Pour conserver les références d’objets en XML, vous avez deux options. L’option la plus simple consiste à ajouter [DataContract(IsReference=true)] à votre classe de modèle. Le paramètre IsReference active les références d’objets. N’oubliez pas que DataContract accepte la sérialisation. Vous devrez donc également ajouter des attributs DataMember aux propriétés :

[DataContract(IsReference=true)]
public class Department
{
    [DataMember]
    public string Name { get; set; }
    [DataMember]
    public Employee Manager { get; set; }
}

À présent, le formateur produit du code XML semblable à ce qui suit :

<Department xmlns:i="http://www.w3.org/2001/XMLSchema-instance" z:Id="i1" 
            xmlns:z="http://schemas.microsoft.com/2003/10/Serialization/" 
            xmlns="http://schemas.datacontract.org/2004/07/Models">
  <Manager>
    <Department z:Ref="i1" />
    <Name>Alice</Name>
  </Manager>
  <Name>Sales</Name>
</Department>

Si vous souhaitez éviter les attributs sur votre classe de modèle, il existe une autre option : Créez un dataContractSerializer spécifique au type instance et définissez preserveObjectReferences sur true dans le constructeur. Définissez ensuite cette instance en tant que sérialiseur par type sur le formateur de type multimédia XML. Le code suivant montre comment procéder :

var xml = GlobalConfiguration.Configuration.Formatters.XmlFormatter;
var dcs = new DataContractSerializer(typeof(Department), null, int.MaxValue, 
    false, /* preserveObjectReferences: */ true, null);
xml.SetSerializer<Department>(dcs);

Test de sérialisation d’objets

Lorsque vous concevez votre API web, il est utile de tester la façon dont vos objets de données seront sérialisés. Vous pouvez le faire sans créer un contrôleur ou appeler une action de contrôleur.

string Serialize<T>(MediaTypeFormatter formatter, T value)
{
    // Create a dummy HTTP Content.
    Stream stream = new MemoryStream();
    var content = new StreamContent(stream);
    /// Serialize the object.
    formatter.WriteToStreamAsync(typeof(T), value, stream, content, null).Wait();
    // Read the serialized string.
    stream.Position = 0;
    return content.ReadAsStringAsync().Result;
}

T Deserialize<T>(MediaTypeFormatter formatter, string str) where T : class
{
    // Write the serialized string to a memory stream.
    Stream stream = new MemoryStream();
    StreamWriter writer = new StreamWriter(stream);
    writer.Write(str);
    writer.Flush();
    stream.Position = 0;
    // Deserialize to an object of type T
    return formatter.ReadFromStreamAsync(typeof(T), stream, null, null).Result as T;
}

// Example of use
void TestSerialization()
{
    var value = new Person() { Name = "Alice", Age = 23 };

    var xml = new XmlMediaTypeFormatter();
    string str = Serialize(xml, value);

    var json = new JsonMediaTypeFormatter();
    str = Serialize(json, value);

    // Round trip
    Person person2 = Deserialize<Person>(json, str);
}