Come mantenere i riferimenti e gestire o ignorare riferimenti circolari in System.Text.Json

Questo articolo illustra come mantenere riferimenti e gestire o ignorare riferimenti circolari durante l'uso di System.Text.Json per serializzare e deserializzare JSON in .NET

Mantenere i riferimenti e gestire riferimenti circolari

Per mantenere i riferimenti e gestire riferimenti circolari, impostare ReferenceHandler su Preserve. Questa impostazione determina il comportamento seguente:

  • In caso di serializzazione:

    Quando si scrivono tipi complessi, il serializzatore scrive anche le proprietà dei metadati ($id, $valuese $ref).

  • In fase di deserializzazione:

    I metadati sono previsti (anche se non obbligatori) e il deserializzatore tenta di comprenderlo.

Il codice seguente illustra l'uso dell'impostazione 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

Questa funzionalità non può essere usata per mantenere i tipi valore o i tipi non modificabili. In caso di deserializzazione, l'istanza di un tipo non modificabile viene creata dopo la lettura dell'intero payload. Pertanto, sarebbe impossibile deserializzare la stessa istanza se viene visualizzato un riferimento all'oggetto all'interno del payload JSON.

Per i tipi valore, i tipi non modificabili e le matrici, non vengono serializzati metadati di riferimento. In caso di deserializzazione, viene generata un'eccezione se viene trovata $ref o $id. Tuttavia, i tipi valore ignorano $id (e $values nel caso delle raccolte) per consentire la deserializzazione dei payload serializzati tramite Newtonsoft.Json. Newtonsoft.Json serializza i metadati per tali tipi.

Per determinare se gli oggetti sono uguali, System.Text.Json usa ReferenceEqualityComparer.Instance, che usa l'uguaglianza dei riferimenti (Object.ReferenceEquals(Object, Object)) anziché l'uguaglianza dei valori (Object.Equals(Object)) durante il confronto di due istanze dell'oggetto.

Per altre informazioni su come serializzare e deserializzare i riferimenti, vedere ReferenceHandler.Preserve.

La classe ReferenceResolver definisce il comportamento che prevede il mantenimento dei riferimenti alla serializzazione e alla deserializzazione. Creare una classe derivata per specificare il comportamento personalizzato. Per un esempio, vedere GuidReferenceResolver.

Rendere persistenti i metadati di riferimento tra più chiamate di serializzazione e deserializzazione

Per impostazione predefinita, i dati di riferimento vengono memorizzati nella cache solo per ogni chiamata a Serialize o Deserialize. Per rendere persistenti i riferimenti da una chiamata Serialize/Deserialize a un'altra, eseguire la radice dell'istanza ReferenceResolver nel sito di chiamata di Serialize/Deserialize. Il codice seguente mostra un esempio per questo scenario:

  • È disponibile un elenco di oggetti Employee ed è necessario serializzare ognuno singolarmente.
  • Si desidera sfruttare i riferimenti salvati nel sistema di risoluzione per il ReferenceHandler.

Questa è la classe Employee :

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

Una classe che deriva da ReferenceResolver archivia i riferimenti in un dizionario:

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 classe che deriva da ReferenceHandler contiene un'istanza di MyReferenceResolver e crea una nuova istanza solo quando necessario (in un metodo denominato Reset in questo esempio):

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

Quando il codice di esempio chiama il serializzatore, usa un'istanza di JsonSerializerOptions in cui la proprietà ReferenceHandler è impostata su un'istanza di MyReferenceHandler. Quando si segue questo modello, assicurarsi di reimpostare il dizionario ReferenceResolver al termine della serializzazione, per evitare che cresca all'infinito.

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();

Ignora riferimenti circolari

Anziché gestire riferimenti circolari, è possibile ignorarli. Per ignorare i riferimenti circolari, impostare ReferenceHandler su IgnoreCycles. Il serializzatore imposta le proprietà di riferimento circolari su null, come illustrato nell'esempio seguente:

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

Nell'esempio precedente, Manager in Adrian King viene serializzato come null per evitare il riferimento circolare. Questo comportamento presenta i vantaggi seguenti rispetto a ReferenceHandler.Preserve:

  • Riduce le dimensioni del payload.
  • Crea JSON comprensibile per i serializzatori diversi da System.Text.Json e Newtonsoft.Json.

Questo comportamento presenta gli svantaggi seguenti:

  • Perdita invisibile all'utente dei dati.
  • I dati non possono eseguire un round trip da JSON all'oggetto di origine.

Vedi anche