Cómo conservar las referencias y administrar o ignorar las referencias circulares en System.Text.Json

Este artículo muestra cómo conservar las referencias y controlar las referencias circulares durante la serialización y deserialización de JSON en .NET al usar System.Text.Json

Conservación de las referencias y administración de las referencias circulares

Para conservar las referencias y administrar las referencias circulares, establezca ReferenceHandler en Preserve. Este valor produce el comportamiento siguiente:

  • Al serializar:

    Al escribir tipos complejos, el serializador también escribe propiedades de metadatos ($id, $values y $ref).

  • Al deserializar:

    Se esperan metadatos (aunque no es obligatorio) y el deserializador intenta entenderlo.

En el siguiente código se muestra el uso del parámetro 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

Esta característica no se puede usar para conservar tipos de valor o tipos inmutables. En la deserialización, se crea la instancia de un tipo inmutable después de leer la carga completa. Por lo tanto, sería imposible deserializar la misma instancia si aparece una referencia a ella dentro de la carga de JSON.

En el caso de los tipos de valor, los tipos inmutables y las matrices, no se serializan los metadatos de referencia. En la deserialización, se produce una excepción si se encuentra $ref o $id. Sin embargo, los tipos de valor omiten $id (y $values en el caso de las colecciones) para que sea posible deserializar las cargas que se serializaron mediante Newtonsoft.Json. Newtonsoft.Json serializa los metadatos de estos tipos.

Para determinar si los objetos son iguales, System.Text.Json usa ReferenceEqualityComparer.Instance, que usa la igualdad de referencia (Object.ReferenceEquals(Object, Object)) en lugar de la igualdad de valores (Object.Equals(Object)) cuando se comparan dos instancias de objeto.

Para obtener más información sobre cómo se serializan y deserializan las referencias, vea ReferenceHandler.Preserve.

La clase ReferenceResolver define el comportamiento de conservar las referencias en la serialización y deserialización. Cree una clase derivada para especificar el comportamiento personalizado. Para obtener un ejemplo, vea GuidReferenceResolver.

Conservar los metadatos de referencia en varias llamadas de serialización y deserialización

De manera predeterminada, los datos de referencia solo se almacenan en caché para cada llamada a Serialize o Deserialize. Para conservar las referencias de una llamada Serialize/Deserialize a otra, coloque la instancia ReferenceResolver en el sitio de llamada de Serialize/Deserialize. El código siguiente muestra un ejemplo de este escenario:

  • Tiene una lista de objetos Employee y debe serializar cada uno individualmente.
  • Quiere aprovechar las referencias guardadas en el solucionador para ReferenceHandler.

Esta es la clase Employee :

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

Una clase que deriva de ReferenceResolver almacena las referencias en un diccionario:

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;
    }
}

Una clase que deriva de ReferenceHandler contiene una instancia de MyReferenceResolver y crea una nueva instancia solo cuando es necesario (en un método de nombre Reset en este ejemplo):

class MyReferenceHandler : ReferenceHandler
{
    public MyReferenceHandler() => Reset();
    private ReferenceResolver? _rootedResolver;
    public override ReferenceResolver CreateResolver() => _rootedResolver!;
    public void Reset() => _rootedResolver = new MyReferenceResolver();
}

Cuando el código de ejemplo llama al serializador, usa una instancia JsonSerializerOptions en la que la propiedad ReferenceHandler se establece en una instancia de MyReferenceHandler. Al seguir este patrón, asegúrese de restablecer el diccionario ReferenceResolver cuando haya terminado de serializar, para evitar que crezca indefinidamente.

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 referencias circulares

En lugar de controlar las referencias circulares, puede omitirlas. Para ignorar las referencias circulares, establezca ReferenceHandler en IgnoreCycles. El serializador establece las propiedades de referencia circulares en null, como se muestra en el ejemplo siguiente:

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

En el ejemplo anterior, Manager en Adrian King se serializa como null para evitar la referencia circular. Este comportamiento tiene las siguientes ventajas con respecto a ReferenceHandler.Preserve:

  • Disminuye el tamaño de la carga.
  • Crea JSON que es comprensible para serializadores distintos de System.Text.Json y Newtonsoft.Json.

Este comportamiento tiene las siguientes desventajas:

  • Pérdida de datos silenciosa.
  • Los datos no pueden realizar un recorrido de ida y vuelta desde JSON al objeto de origen.

Vea también