Jak zachovat odkazy a zpracovat nebo ignorovat cyklický odkaz v System.Text.Json

Tento článek ukazuje, jak zachovat odkazy a zpracovat nebo ignorovat cyklické odkazy při použití System.Text.Json k serializaci a deserializaci JSON v .NET.

Zachování odkazů a zpracování cyklických odkazů

Chcete-li zachovat odkazy a zpracovat cyklický odkaz, nastavte na Preservehodnotu ReferenceHandler . Toto nastavení způsobuje následující chování:

  • Při serializaci:

    Při psaní složitých typů serializátor také zapisuje vlastnosti metadat ($id, $valuesa $ref).

  • Při deserializaci:

    Očekává se metadata (i když není povinná) a deserializátor se ji pokusí pochopit.

Použití tohoto nastavení ukazuje Preserve následující kód.

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

Tuto funkci nelze použít k zachování typů hodnot nebo neměnných typů. Při deserializaci se vytvoří instance neměnného typu po načtení celé datové části. Proto by nebylo možné deserializovat stejnou instanci, pokud se v datové části JSON zobrazí odkaz na ni.

U hodnotových typů, neměnných typů a polí není serializována žádná referenční metadata. Při deserializaci je vyvolána výjimka, pokud $ref nebo $id je nalezena. Typy hodnot však ignorují $id (a $values v případě kolekcí), aby bylo možné deserializovat datové části, které byly serializovány pomocí Newtonsoft.Json. Newtonsoft.Json serializuje metadata pro tyto typy.

Chcete-li určit, zda jsou objekty stejné, System.Text.Json používá ReferenceEqualityComparer.Instancepři porovnávání dvou instancí objektů rovnost odkazu (Object.ReferenceEquals(Object, Object)) místo rovnosti hodnot (Object.Equals(Object)).

Další informace o tom, jak jsou odkazy serializovány a deserializovány, naleznete v tématu ReferenceHandler.Preserve.

Třída ReferenceResolver definuje chování zachování odkazů na serializaci a deserializaci. Vytvořte odvozenou třídu pro určení vlastního chování. Příklad najdete v tématu GuidReferenceResolver.

Zachování referenčních metadat napříč více voláními serializace a deserializace

Ve výchozím nastavení se referenční data ukládají pouze do mezipaměti pro každé volání nebo SerializeDeserialize. Chcete-li zachovat odkazy z jednoho Serialize/Deserialize volání do druhého, kořen instance ReferenceResolver v lokalitě Serialize/Deserializevolání . Následující kód ukazuje příklad pro tento scénář:

  • Máte seznam Employee objektů a musíte serializovat každý z nich jednotlivě.
  • Chcete využít odkazy uložené v překladače pro ReferenceHandler.

Tady je Employee třída:

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

Třída odvozená od ReferenceResolver úložišť odkazů ve slovníku:

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

Třída, která je odvozena z ReferenceHandler blokování instance MyReferenceResolver a vytváří novou instanci pouze v případě potřeby (v metodě pojmenované Reset v tomto příkladu):

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

Když vzorový kód volá serializátor, používá JsonSerializerOptions instanci, ve které ReferenceHandler je vlastnost nastavena na instanci MyReferenceHandler. Když budete postupovat podle tohoto vzoru, nezapomeňte po dokončení serializace obnovit ReferenceResolver slovník, aby se nezvětšil navždy.

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

Ignorování cyklických odkazů

Místo zpracování cyklických odkazů je můžete ignorovat. Chcete-li ignorovat cyklický odkaz, nastavte ReferenceHandler hodnotu IgnoreCycles. Serializátor nastaví vlastnosti cyklický odkaz , nulljak je znázorněno v následujícím příkladu:

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

V předchozím příkladu je pod Adrian King serializován tak, Manager aby null se zabránilo cyklický odkaz. Toto chování má následující výhody ReferenceHandler.Preserve:

  • Zmenší velikost datové části.
  • Vytvoří JSON, který je srozumitelný pro serializátory jiné než System.Text.Json a Newtonsoft.Json.

Toto chování má následující nevýhody:

  • Bezobslužná ztráta dat.
  • Data nemůžou provést zpáteční cestu z JSON zpět do zdrojového objektu.

Viz také