Como preservar referências e manipular ou ignorar referências circulares em System.Text.Json
Este artigo mostra como preservar referências e manipular ou ignorar referências circulares ao usar System.Text.Json para serializar e desserializar JSON no .NET
Preservar referências e lidar com referências circulares
Para preservar referências e lidar com referências circulares, defina ReferenceHandler como Preserve. Essa configuração causa o seguinte comportamento:
Ao serializar:
Ao escrever tipos complexos, o serializador também grava propriedades de metadados (
$id
,$values
e$ref
).Ao desserializar:
Metadados são esperados (embora não obrigatórios) e o desserializador tenta entendê-los.
O código a seguir ilustra o uso da configuração 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
Esse recurso não pode ser usado para preservar tipos de valor ou tipos imutáveis. Na desserialização, a instância de um tipo imutável é criada depois que toda a carga útil é lida. Portanto, seria impossível desserializar a mesma instância se uma referência a ela aparecesse na carga JSON.
Para tipos de valor, tipos imutáveis e matrizes, nenhum metadados de referência é serializado. Na desserialização, uma exceção é lançada se $ref
ou $id
for encontrado. No entanto, os tipos de valor ignoram $id
(e $values
no caso de coleções) para possibilitar desserializar cargas que foram serializadas usando Newtonsoft.Json, que serializa metadados para tais tipos.
Para determinar se os objetos são iguais, System.Text.Json usa ReferenceEqualityComparer.Instance, que usa igualdade de referência (Object.ReferenceEquals(Object, Object)) em vez de igualdade de valor (Object.Equals(Object)) ao comparar duas instâncias de objeto.
Para obter mais informações sobre como as referências são serializadas e desserializadas, consulte ReferenceHandler.Preserve.
A classe ReferenceResolver define o comportamento de preservação de referências na serialização e desserialização. Crie uma classe derivada para especificar o comportamento personalizado. Para obter um exemplo, consulte GuidReferenceResolver.
Persistir metadados de referência em várias chamadas de serialização e desserialização
Por padrão, os dados de referência só são armazenados em cache para cada chamada a Serialize ou Deserialize. Para persistir referências de uma chamada Serialize
ou Deserialize
para outra, tenha como raiz a instância ReferenceResolver no site de chamada de Serialize
/Deserialize
. O seguinte código mostra um exemplo para esse cenário:
- Você tem uma lista de objetos
Employee
e precisa serializar cada um individualmente. - Você deseja aproveitar as referências salvas no resolvedor para o
ReferenceHandler
.
Aqui está a classe Employee
:
public class Employee
{
public string? Name { get; set; }
public Employee? Manager { get; set; }
public List<Employee>? DirectReports { get; set; }
}
Uma classe derivada de ReferenceResolver armazena as referências em um dicionário:
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;
}
}
Uma classe derivada de ReferenceHandler contém uma instância e MyReferenceResolver
cria uma nova instância somente quando necessário (em um método chamado Reset
neste exemplo):
class MyReferenceHandler : ReferenceHandler
{
public MyReferenceHandler() => Reset();
private ReferenceResolver? _rootedResolver;
public override ReferenceResolver CreateResolver() => _rootedResolver!;
public void Reset() => _rootedResolver = new MyReferenceResolver();
}
Quando o código de exemplo chama o serializador, ele usa uma instância JsonSerializerOptions na qual a propriedade ReferenceHandler é definida como uma instância de MyReferenceHandler
. Ao seguir esse padrão, redefina o dicionário ReferenceResolver
ao terminar de serializar para evitar que ele continue crescendo para sempre.
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();
Ignorar referências circulares
Em vez de manipular referências circulares, você pode ignorá-las. Ignorar referências circulares, defina ReferenceHandler como IgnoreCycles. O serializador define as propriedades de referência circular como null
, conforme mostrado no exemplo a seguir:
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
No exemplo anterior, Manager
em Adrian King
é serializado como null
para evitar a referência circular. Esse comportamento tem as seguintes vantagens sobre ReferenceHandler.Preserve:
- Diminui o tamanho da carga útil.
- Ele cria JSON que é compreensível para serializadores diferentes de System.Text.Json e Newtonsoft.Json.
Esse comportamento tem as seguintes desvantagens:
- Perda silenciosa de dados.
- Os dados não podem fazer uma viagem de ida e volta do JSON de volta ao objeto de origem.
Confira também
- Visão geral de System.Text.Json
- Como serializar e desserializar JSON