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 manipular referências circulares, defina ReferenceHandler como Preserve. Essa configuração causa o seguinte comportamento:

  • Na serialização:

    Ao escrever tipos complexos, o serializador também grava propriedades de metadados ($id, $valuese $ref).

  • Ao desserializar:

    Os metadados são esperados (embora não obrigatórios), e o desserializador tenta compreendê-los.

O código a seguir ilustra o Preserve uso da configuração.

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 é lida. Portanto, seria impossível desserializar a mesma instância se uma referência a ela aparecer dentro da carga JSON útil.

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 encontrada. No entanto, os tipos de valor ignoram $id (e $values no caso de coleções) para tornar possível desserializar cargas úteis que foram serializadas usando Newtonsoft.Json, que serializa metadados para esses 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 ReferenceResolver classe define o comportamento de preservar referências em serialização e desserialização. Crie uma classe derivada para especificar o comportamento personalizado. Para obter um exemplo, consulte GuidReferenceResolver.

Persista metadados de referência em várias chamadas de serialização e desserialização

Por padrão, os dados de referência são armazenados em cache apenas para cada chamada para Serialize ou Deserialize. Para persistir referências de uma Serialize ou chamada para outra, faça root na ReferenceResolver instância no site de chamada de Serialize/Deserialize.Deserialize O código a seguir mostra um exemplo para esse cenário:

  • Você tem uma lista de Employee objetos e você tem que serializar cada um individualmente.
  • Você deseja aproveitar as referências salvas no resolvedor para o ReferenceHandler.

Aqui está a Employee classe:

public class Employee
{
    public string? Name { get; set; }
    public Employee? Manager { get; set; }
    public List<Employee>? DirectReports { get; set; }
}

Uma classe que deriva 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 que deriva de ReferenceHandler mantém uma instância de MyReferenceResolver e cria uma nova instância somente quando necessário (em um método nomeado 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 JsonSerializerOptions instância na qual a ReferenceHandler propriedade é definida como uma instância de MyReferenceHandler. Ao seguir esse padrão, certifique-se de redefinir o ReferenceResolver dicionário quando terminar de serializar, para evitar que ele cresça 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 lidar com referências circulares, você pode ignorá-las. Para ignorar referências circulares, defina ReferenceHandler como IgnoreCycles. O serializador define propriedades de referência circular como , conforme nullmostrado 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 under Adrian King é serializado para null 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.

Consulte também