Jak zachować odwołania i obsługiwać lub ignorować odwołania cykliczne w programie System.Text.Json
W tym artykule pokazano, jak zachować odwołania i obsługiwać lub ignorować odwołania cykliczne podczas używania System.Text.Json do serializacji i deserializacji kodu JSON na platformie .NET
Zachowywanie odwołań i obsługa odwołań cyklicznych
Aby zachować odwołania i obsłużyć odwołania cykliczne, ustaw wartość ReferenceHandler Preserve. To ustawienie powoduje następujące zachowanie:
W przypadku serializacji:
Podczas pisania typów złożonych serializator zapisuje również właściwości metadanych (
$id
,$values
, i$ref
).Podczas deserializacji:
Metadane są oczekiwane (choć nie obowiązkowe), a deserializator próbuje je zrozumieć.
Poniższy kod ilustruje użycie Preserve
ustawienia.
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
Tej funkcji nie można używać do zachowywania typów wartości ani typów niezmiennych. W przypadku deserializacji wystąpienie niezmiennego typu jest tworzone po odczytaniu całego ładunku. Dlatego deserializowanie tego samego wystąpienia byłoby niemożliwe, jeśli odwołanie do niego pojawia się w ładunku JSON.
W przypadku typów wartości, niezmiennych typów i tablic żadne metadane referencyjne nie są serializowane. W przypadku deserializacji zgłaszany jest wyjątek, jeśli $ref
zostanie znaleziony lub $id
znaleziony. Jednak typy wartości ignorują $id
(i $values
w przypadku kolekcji), aby umożliwić deserializowanie ładunków, które zostały serializowane przy użyciu metody Newtonsoft.Json, która serializuje metadane dla takich typów.
Aby określić, czy obiekty są równe, System.Text.Json używa metody ReferenceEqualityComparer.Instance, która używa równości odwołań () zamiast równości wartości (Object.ReferenceEquals(Object, Object)Object.Equals(Object)) podczas porównywania dwóch wystąpień obiektów.
Aby uzyskać więcej informacji na temat serializacji i deserializacji odwołań, zobacz ReferenceHandler.Preserve.
Klasa ReferenceResolver definiuje zachowanie zachowania odwołań do serializacji i deserializacji. Utwórz klasę pochodną, aby określić zachowanie niestandardowe. Aby zapoznać się z przykładem, zobacz GuidReferenceResolver.
Utrwalanie metadanych odwołania między wieloma wywołaniami serializacji i deserializacji
Domyślnie dane referencyjne są buforowane tylko dla każdego wywołania metody Serialize lub Deserialize. Aby utrwały odwołania z jednego Serialize
lub Deserialize
wywołania do innego, należy ReferenceResolver root wystąpienia w lokacjiDeserialize
Serialize
/ wywołania klasy . Poniższy kod przedstawia przykład dla tego scenariusza:
- Masz listę
Employee
obiektów i musisz serializować każdy z nich indywidualnie. - Chcesz skorzystać z odwołań zapisanych w narzędziu rozpoznawania nazw dla elementu
ReferenceHandler
.
Employee
Oto klasa:
public class Employee
{
public string? Name { get; set; }
public Employee? Manager { get; set; }
public List<Employee>? DirectReports { get; set; }
}
Klasa, która pochodzi z ReferenceResolver magazynów odwołań w słowniku:
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;
}
}
Klasa, która pochodzi z ReferenceHandler MyReferenceResolver
wystąpienia klasy i tworzy nowe wystąpienie tylko wtedy, gdy jest to konieczne (w metodzie o nazwie Reset
w tym przykładzie):
class MyReferenceHandler : ReferenceHandler
{
public MyReferenceHandler() => Reset();
private ReferenceResolver? _rootedResolver;
public override ReferenceResolver CreateResolver() => _rootedResolver!;
public void Reset() => _rootedResolver = new MyReferenceResolver();
}
Gdy przykładowy kod wywołuje serializator, używa JsonSerializerOptions wystąpienia, w którym ReferenceHandler właściwość jest ustawiona na wystąpienie MyReferenceHandler
klasy . Po przestrzeganiu tego wzorca pamiętaj, aby zresetować słownik po zakończeniu ReferenceResolver
serializacji, aby zachować jego rozwój na zawsze.
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();
Ignoruj odwołania cykliczne
Zamiast obsługiwać odwołania cykliczne, można je zignorować. Aby zignorować odwołania cykliczne, ustaw wartość ReferenceHandler IgnoreCycles. Serializator ustawia właściwości odwołania cyklicznego na null
, jak pokazano w poniższym przykładzie:
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
W poprzednim przykładzie Manager
element under Adrian King
jest serializowany, aby null
uniknąć odwołania cyklicznego. To zachowanie ma następujące zalety w porównaniu z ReferenceHandler.Preserve:
- Zmniejsza rozmiar ładunku.
- Tworzy kod JSON zrozumiały dla serializatorów innych niż System.Text.Json i Newtonsoft.Json.
To zachowanie ma następujące wady:
- Dyskretna utrata danych.
- Dane nie mogą wykonać rundy z pliku JSON z powrotem do obiektu źródłowego.