1. Introduction
Imaginez : vous avez écrit la classe Person, sérialisé un objet dans un fichier, et après quelques mois vous décidez qu'il lui faut, disons, une nouvelle adresse de résidence ou vous avez changé le type de certaines propriétés. Ça semble banal — mais quand vous essayez de charger (désérialiser) des données précédemment sauvegardées au format ancien, une surprise peut vous attendre : quelque chose ne se chargera pas, une exception sera lancée, certaines valeurs seront vides ou même incorrectes.
Ce comportement est un cas typique de rupture de compatibilité inverse. En développement réel ça arrive plus souvent que les étudiants n'oublient de mettre un point-virgule (c'est-à-dire très souvent).
Présentons le problème par un exemple
Considérons notre mini-projet pédagogique. Supposons qu'à ce stade nous avions la classe suivante :
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
On sérialise une instance de cette classe en JSON :
Person p = new Person { Name = "Alice", Age = 35 };
string json = JsonSerializer.Serialize(p);
File.WriteAllText("person.json", json);
Dans le fichier on obtient :
{"Name":"Alice","Age":35}
Maintenant, une semaine plus tard, on décide de rendre notre application plus "moderne" en ajoutant un champ adresse :
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public string Address { get; set; } // nouveau champ
}
Puis — essayons de charger l'ancien fichier :
string json = File.ReadAllText("person.json");
Person p = JsonSerializer.Deserialize<Person>(json);
Que se passera-t-il ? L'adresse ne sera pas présente dans notre objet : la propriété Address vaudra null. Aucune erreur n'est survenue. Pour l'instant tout fonctionne... Mais si vous commencez à changer des types, supprimer des champs ou faire quelque chose de vraiment "intéressant", des problèmes peuvent apparaître !
2. Quels types de changements existent — et comment ils impactent
Les changements dans la structure des classes impactent la sérialisation différemment. Voyons quelques scénarios typiques.
Ajout de nouvelles propriétés
C'est l'option la moins dangereuse. Les anciennes données (où ces propriétés n'existaient pas) se désérialisent sans problème : les nouvelles propriétés prendront les valeurs par défaut (null pour les références, 0 pour int, etc.).
Attention : Si votre nouvelle propriété n'est pas nullable et n'a pas de valeur par défaut "raisonnable", un problème peut survenir (surtout avec les propriétés required de C# 11+).
Suppression de propriétés
Si vous supprimez une propriété alors que les données sérialisées l'ont encore — le sérialiseur va très probablement juste ignorer le "surplus", et le chargement se fera quand même.
Mais cela dépend du sérialiseur utilisé. Par exemple, JsonSerializer et Newtonsoft.Json sont assez tolérants : ils ne lèveront pas d'exception, alors que certains sérialiseurs anciens ou custom peuvent se comporter différemment.
Renommage de propriétés
Là commence la fête. Si vous renommez simplement la propriété FirstName en Name, le sérialiseur ne pourra pas faire le mapping entre les champs des anciennes données et le nouvel objet. Autrement dit, le champ sera vide (null/0) et l'ancien dans le fichier sera ignoré.
Changement du type d'une propriété
Par exemple, auparavant vous aviez public int Age, puis vous décidez d'en faire public string Age (au cas où quelqu'un écrirait "immortal" — ça arrive). La tentative de désérialiser les anciennes données peut provoquer une erreur ("Cannot convert number to string") ou la propriété obtiendra simplement la valeur par défaut. Tout dépend du sérialiseur et de ses paramètres de typage strict.
Modification des hiérarchies (héritage, imbrication)
Si vous changez les classes de base, déplacez des propriétés ailleurs ou, par exemple, faites une classe wrapper pour une autre — les anciennes données sérialisées peuvent devenir complètement incompatibles. Cela concerne particulièrement XML et les hiérarchies d'objets complexes.
3. Problèmes de compatibilité
Comment détecter les problèmes de compatibilité ?
Souvent l'erreur de compatibilité n'apparaît pas tout de suite et pas clairement : votre appli commence simplement à se comporter "étrangement", certaines données se perdent, ou un message d'exception pas très informatif apparaît dans les logs. Le plus souvent les problèmes surgissent quand :
- L'utilisateur charge un ancien fichier dans une nouvelle version du programme.
- Le serveur reçoit du JSON/XML d'un client "ancienne version".
- Vous travaillez avec une API externe dont l'interface a été mise à jour soudainement.
Les symptômes varient : des erreurs lors de la désérialisation jusqu'à des champs "inattendus" vides.
Impact du sérialiseur sur la compatibilité
Tous les sérialiseurs ne se comportent pas de la même façon. Les sérialiseurs JSON sont les plus "tolérants" aux changements de structure — aussi bien le standard System.Text.Json que Newtonsoft.Json. Ils ont tendance à ignorer les propriétés inconnues du fichier et à ne pas sérialiser les champs inconnus de l'objet.
Avec XML c'est un peu plus strict : si l'élément racine ou la hiérarchie change, des erreurs peuvent apparaître.
Dans les formats binaires il est même possible d'avoir des exceptions si l'ordre ou les types ont changé !
4. Comment minimiser les risques ? Approches et bonnes pratiques
Voici quelques approches qui aident à réduire au minimum les ennuis (et parfois à les éviter complètement).
Utilisez des versions pour les classes et les données
Ajoutez un champ spécial Version dans les objets sérialisables ou dans les fichiers eux-mêmes. Cela permet de déterminer avec quelle version de la structure le fichier a été créé et de prendre la décision adéquate au chargement (par exemple effectuer une mise à niveau des données).
public class PersonV2
{
public int Version { get; set; } = 2;
public string Name { get; set; }
public int Age { get; set; }
public string Address { get; set; }
}
Appliquez des attributs de mappage de noms (pour la sérialisation)
Avec JSON et XML on peut indiquer explicitement comment une propriété doit s'appeler dans la forme sérialisée. Si vous renommez une propriété — conservez l'ancien nom :
public class Person
{
[JsonPropertyName("FirstName")] // pour System.Text.Json
[JsonProperty("FirstName")] // pour Newtonsoft.Json
public string Name { get; set; }
public int Age { get; set; }
}
Utilisez des types nullable et des valeurs par défaut
Si de nouveaux champs apparaissent et qu'ils ne sont pas toujours présents dans les anciennes données — faites-les nullable ou donnez-leur une valeur par défaut pour que la désérialisation ne pose pas de problème :
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public string? Address { get; set; } = "Inconnu";
}
Gestion des événements "champ inconnu"
Avec Newtonsoft.Json on peut souscrire à la gestion des champs "incompréhensibles" via un handler spécial, pour par exemple logger une situation potentiellement dangereuse.
var settings = new JsonSerializerSettings
{
MissingMemberHandling = MissingMemberHandling.Error
};
try
{
var person = JsonConvert.DeserializeObject<Person>(json, settings);
}
catch (JsonSerializationException ex)
{
Console.WriteLine("Impossible de désérialiser : " + ex.Message);
}
Migration des données
Si les changements sont substantiels, il est plus raisonnable de prévoir une étape de migration : par exemple, charger les données dans la "vieille" structure puis les transformer en nouvelle :
// Supposons que PersonV1 n'avait pas address
public class PersonV1 { public string Name; public int Age; }
// Nouveau — avec address
public class PersonV2 { public string Name; public int Age; public string Address; }
// Migration :
string oldJson = File.ReadAllText("person.json");
PersonV1 oldPerson = JsonSerializer.Deserialize<PersonV1>(oldJson);
PersonV2 migrated = new PersonV2
{
Name = oldPerson.Name,
Age = oldPerson.Age,
Address = "Inconnu"
};
5. Cas complexes et erreurs inattendues
Invariant d'un champ et propriétés required
Avec C# 11 et versions supérieures sont apparues les propriétés required. Maintenant si un champ est marqué comme required, la désérialisation peut lancer une erreur si ce champ manque dans les données :
public class Person
{
public string Name { get; set; }
[JsonPropertyName("Age")]
public required int Age { get; set; }
public string Address { get; set; }
}
Si dans les anciennes données le champ Age est absent — une exception sur l'incompatibilité de la structure se produira.
Changement de type : int → string
// Avant :
public class Record { public int Count; }
// Après :
public class Record { public string Count; }
Si dans les données on a "Count":42, la désérialisation vers string peut parfois marcher (conversion intelligente), mais dans l'autre sens — une exception peut survenir.
Suppression d'une classe de base
Si l'objet sérialisé héritait d'une classe et que la hiérarchie a été modifiée — la désérialisation des anciens fichiers peut provoquer des erreurs, parfois "silencieuses", parfois explicites.
6. Erreurs typiques lors du travail sur la compatibilité
Erreur n°1 : modifier sans réfléchir des propriétés existantes.
Renommer ou changer le type d'une propriété sans tenir compte des données déjà sérialisées entraîne une perte d'information lors de la désérialisation.
Erreur n°2 : oublier les types nullable pour les nouveaux champs.
Les nouvelles propriétés doivent être soit nullable, soit avoir des valeurs par défaut raisonnables.
Erreur n°3 : ne pas tester la compatibilité inverse.
Vous changez une classe — testez obligatoirement que les anciens fichiers/données se chargent toujours correctement.
Erreur n°4 : mélanger les attributs de différentes bibliothèques.
Ne mettez pas JsonPropertyName et JsonProperty en même temps sur une même propriété.
GO TO FULL VERSION