Comment conserver des références et gérer ou ignorer les références circulaires dans System.Text.Json
Cet article explique comment conserver les références et gérer ou ignorer les références circulaires lors de l’utilisation de System.Text.Json pour sérialiser et désérialiser du JSON dans .NET
Conserver les références et gérer les références circulaires
Pour conserver les références et gérer les références circulaires, définissez ReferenceHandler sur Preserve. Ce paramètre entraîne le comportement suivant :
Lors de la sérialisation :
Lors de l’écriture de types complexes, le sérialiseur écrit également les propriétés de métadonnées (
$id
,$values
et$ref
).Lors de la désérialisation :
Des métadonnées sont attendues (bien qu’elles ne soient pas obligatoires), et le désérialiseur tente de les comprendre.
Le code suivant illustre l’utilisation du paramètre Preserve
.
using System.Text.Json;
using System.Text.Json.Serialization;
namespace PreserveReferences
{
public class Employee
{
public string? Name { get; set; }
public Employee? Manager { get; set; }
public List<Employee>? DirectReports { get; set; }
}
public class Program
{
public static void Main()
{
Employee tyler = new()
{
Name = "Tyler Stein"
};
Employee adrian = new()
{
Name = "Adrian King"
};
tyler.DirectReports = [adrian];
adrian.Manager = tyler;
JsonSerializerOptions options = new()
{
ReferenceHandler = ReferenceHandler.Preserve,
WriteIndented = true
};
string tylerJson = JsonSerializer.Serialize(tyler, options);
Console.WriteLine($"Tyler serialized:\n{tylerJson}");
Employee? tylerDeserialized =
JsonSerializer.Deserialize<Employee>(tylerJson, options);
Console.WriteLine(
"Tyler is manager of Tyler's first direct report: ");
Console.WriteLine(
tylerDeserialized?.DirectReports?[0].Manager == tylerDeserialized);
}
}
}
// Produces output like the following example:
//
//Tyler serialized:
//{
// "$id": "1",
// "Name": "Tyler Stein",
// "Manager": null,
// "DirectReports": {
// "$id": "2",
// "$values": [
// {
// "$id": "3",
// "Name": "Adrian King",
// "Manager": {
// "$ref": "1"
// },
// "DirectReports": null
// }
// ]
// }
//}
//Tyler is manager of Tyler's first direct report:
//True
Imports System.Text.Json
Imports System.Text.Json.Serialization
Namespace PreserveReferences
Public Class Employee
Public Property Name As String
Public Property Manager As Employee
Public Property DirectReports As List(Of Employee)
End Class
Public NotInheritable Class Program
Public Shared Sub Main()
Dim tyler As New Employee
Dim adrian As New Employee
tyler.DirectReports = New List(Of Employee) From {
adrian}
adrian.Manager = tyler
Dim options As New JsonSerializerOptions With {
.ReferenceHandler = ReferenceHandler.Preserve,
.WriteIndented = True
}
Dim tylerJson As String = JsonSerializer.Serialize(tyler, options)
Console.WriteLine($"Tyler serialized:{tylerJson}")
Dim tylerDeserialized As Employee = JsonSerializer.Deserialize(Of Employee)(tylerJson, options)
Console.WriteLine(
"Tyler is manager of Tyler's first direct report: ")
Console.WriteLine(
tylerDeserialized.DirectReports(0).Manager Is tylerDeserialized)
End Sub
End Class
End Namespace
' Produces output like the following example:
'
'Tyler serialized:
'{
' "$id": "1",
' "Name": "Tyler Stein",
' "Manager": null,
' "DirectReports": {
' "$id": "2",
' "$values": [
' {
' "$id": "3",
' "Name": "Adrian King",
' "Manager": {
' "$ref": "1"
' },
' "DirectReports": null
' }
' ]
' }
'}
'Tyler is manager of Tyler's first direct report:
'True
Cette fonctionnalité ne peut pas être utilisée pour conserver des types valeur ou des types immuables. Lors de la désérialisation, l’instance d’un type immuable est créée après la lecture de la charge utile entière. Il serait donc impossible de désérialiser la même instance si une référence à celle-ci apparaît dans la charge utile JSON.
Pour les types valeur, les types immuables et les tableaux, aucune métadonnée de référence n’est sérialisée. Lors de la désérialisation, une exception est levée si $ref
ou $id
est trouvé. Cependant, les types valeurs ignorent $id
(et $values
dans le cas des collections) afin de permettre la désérialisation de charges utiles qui ont été sérialisées en utilisant Newtonsoft.Json, lequel sérialise bien les métadonnées pour ces types.
Pour déterminer si les objets sont égaux, System.Text.Json utilise ReferenceEqualityComparer.Instance, qui utilise l’égalité de référence (Object.ReferenceEquals(Object, Object)) au lieu de l’égalité de valeur (Object.Equals(Object)) lors de la comparaison de deux instances d’objet.
Pour plus d’informations sur la façon dont les références sont sérialisées et désérialisées, consultez ReferenceHandler.Preserve.
La classe ReferenceResolver définit le comportement de la préservation des références en cas de sérialisation et désérialisation. Créez une classe dérivée pour spécifier un comportement personnalisé. Pour obtenir un exemple, consultez GuidReferenceResolver.
Conserver les métadonnées de référence sur plusieurs appels de sérialisation et de désérialisation
Par défaut, les données de référence sont uniquement mises en cache pour chaque appel à Serialize ou Deserialize. Pour conserver les références d’un appel de Serialize
ou Deserialize
à un autre, ancrez l’instance ReferenceResolver au site d’appel de Serialize
/Deserialize
. Le code suivant présente un exemple pour ce scénario :
- Vous disposez d’une liste d’objets
Employee
et vous devez sérialiser chacun d’eux individuellement. - Vous souhaitez tirer parti des références enregistrées dans le résolveur pour le
ReferenceHandler
.
Voici la classe Employee
:
public class Employee
{
public string? Name { get; set; }
public Employee? Manager { get; set; }
public List<Employee>? DirectReports { get; set; }
}
Une classe qui dérive de ReferenceResolver stocke les références dans un dictionnaire :
class MyReferenceResolver : ReferenceResolver
{
private uint _referenceCount;
private readonly Dictionary<string, object> _referenceIdToObjectMap = [];
private readonly Dictionary<object, string> _objectToReferenceIdMap = new (ReferenceEqualityComparer.Instance);
public override void AddReference(string referenceId, object value)
{
if (!_referenceIdToObjectMap.TryAdd(referenceId, value))
{
throw new JsonException();
}
}
public override string GetReference(object value, out bool alreadyExists)
{
if (_objectToReferenceIdMap.TryGetValue(value, out string? referenceId))
{
alreadyExists = true;
}
else
{
_referenceCount++;
referenceId = _referenceCount.ToString();
_objectToReferenceIdMap.Add(value, referenceId);
alreadyExists = false;
}
return referenceId;
}
public override object ResolveReference(string referenceId)
{
if (!_referenceIdToObjectMap.TryGetValue(referenceId, out object? value))
{
throw new JsonException();
}
return value;
}
}
Une classe qui dérive de ReferenceHandler contient une instance de MyReferenceResolver
et crée une nouvelle instance uniquement si nécessaire (dans une méthode nommée Reset
dans cet exemple) :
class MyReferenceHandler : ReferenceHandler
{
public MyReferenceHandler() => Reset();
private ReferenceResolver? _rootedResolver;
public override ReferenceResolver CreateResolver() => _rootedResolver!;
public void Reset() => _rootedResolver = new MyReferenceResolver();
}
Lorsque l’exemple de code appelle le sérialiseur, il utilise une instance JsonSerializerOptions dans laquelle la propriété ReferenceHandler est définie sur une instance de MyReferenceHandler
. Lorsque vous suivez ce modèle, veillez à réinitialiser le dictionnaire ReferenceResolver
lorsque vous avez terminé la sérialisation, pour éviter qu’il continue à grandir.
var options = new JsonSerializerOptions
{
WriteIndented = true
};
var myReferenceHandler = new MyReferenceHandler();
options.ReferenceHandler = myReferenceHandler;
string json;
foreach (Employee emp in employees)
{
json = JsonSerializer.Serialize(emp, options);
DoSomething(json);
}
// Reset after serializing to avoid out of bounds memory growth in the resolver.
myReferenceHandler.Reset();
Ignorer les références circulaires
Au lieu de gérer les références circulaires, vous pouvez les ignorer. Pour ignorer les références circulaires, définissez ReferenceHandler sur IgnoreCycles. Le sérialiseur définit les propriétés de référence circulaires sur null
, comme illustré dans l’exemple suivant :
using System.Text.Json;
using System.Text.Json.Serialization;
namespace SerializeIgnoreCycles
{
public class Employee
{
public string? Name { get; set; }
public Employee? Manager { get; set; }
public List<Employee>? DirectReports { get; set; }
}
public class Program
{
public static void Main()
{
Employee tyler = new()
{
Name = "Tyler Stein"
};
Employee adrian = new()
{
Name = "Adrian King"
};
tyler.DirectReports = new List<Employee> { adrian };
adrian.Manager = tyler;
JsonSerializerOptions options = new()
{
ReferenceHandler = ReferenceHandler.IgnoreCycles,
WriteIndented = true
};
string tylerJson = JsonSerializer.Serialize(tyler, options);
Console.WriteLine($"Tyler serialized:\n{tylerJson}");
Employee? tylerDeserialized =
JsonSerializer.Deserialize<Employee>(tylerJson, options);
Console.WriteLine(
"Tyler is manager of Tyler's first direct report: ");
Console.WriteLine(
tylerDeserialized?.DirectReports?[0]?.Manager == tylerDeserialized);
}
}
}
// Produces output like the following example:
//
//Tyler serialized:
//{
// "Name": "Tyler Stein",
// "Manager": null,
// "DirectReports": [
// {
// "Name": "Adrian King",
// "Manager": null,
// "DirectReports": null
// }
// ]
//}
//Tyler is manager of Tyler's first direct report:
//False
Dans l’exemple précédent, Manager
sous Adrian King
est sérialisé comme null
pour éviter la référence circulaire. Ce comportement présente les avantages suivants par rapport à ReferenceHandler.Preserve :
- Cela diminue la taille de la charge utile.
- Cela crée un JSON compréhensible pour les sérialiseurs autres que System.Text.Json et Newtonsoft.Json.
Ce comportement présente les inconvénients suivants :
- Perte silencieuse de données.
- Les données ne peuvent pas effectuer d’aller-retour de JSON à l’objet source.