Schreiben von benutzerdefinierten Konvertern für die JSON-Serialisierung (Marshallen) in .NET

In diesem Artikel wird gezeigt, wie Sie benutzerdefinierte Konverter für JSON-Serialisierungsklassen erstellen, die im System.Text.Json-Namespace bereitgestellt werden. Eine Einführung zu System.Text.Json finden Sie unter Serialisieren und Deserialisieren von JSON-Daten in .NET.

Ein Konverter ist eine Klasse, die ein Objekt oder einen Wert in und aus JSON konvertiert. Der System.Text.Json-Namespace verfügt über integrierte Konverter für die meisten primitiven Typen, die JavaScript-Primitiven entsprechen. Sie können benutzerdefinierte Konverter schreiben:

  • Um das Standardverhalten eines integrierten Konverters außer Kraft zu setzen. Sie könnten beispielsweise beschließen, dass DateTime-Werte im Format MM/TT/JJJJ dargestellt werden sollen. Standardmäßig wird ISO 8601-1:2019 einschließlich des Profils RFC 3339 unterstützt. Weitere Informationen finden Sie unter Unterstützung von „DateTime“ und „DateTimeOffset“ in System.Text.Json.
  • Um einen benutzerdefinierten Werttyp zu unterstützen. Beispielsweise eine PhoneNumber-Struktur.

Sie können auch benutzerdefinierte Konverter schreiben, um System.Text.Json mit Funktionen anzupassen oder zu erweitern, die nicht im aktuellen Release enthalten sind. Folgende Szenarien werden später in diesem Artikel abgedeckt:

Berücksichtigen Sie beim Programmieren eines benutzerdefinierten Konverters die erheblichen Leistungseinbußen, die bei der Verwendung neuer JsonSerializerOptions-Instanzen entstehen. Weitere Informationen finden Sie unter Wiederverwenden von JsonSerializerOptions-Instanzen.

Visual Basic kann nicht verwendet werden, um benutzerdefinierte Konverter zu schreiben, aber Konverter aufrufen können, die in C#-Bibliotheken implementiert werden. Weitere Informationen finden Sie unter Visual Basic Support.

Benutzerdefinierte Konvertermuster

Es gibt zwei Muster zum Erstellen eines benutzerdefinierten Konverters: das grundlegende Muster und das Factorymuster. Das Factorymuster ist für Konverter vorgesehen, die den Enum-Typ oder offene generische Typen verarbeiten. Das grundlegende Muster ist für nicht generische und geschlossene generische Typen gedacht. Konverter für die folgenden Typen erfordern z. B. das Factorymuster:

Einige Beispiele für Typen, die vom grundlegenden Muster verarbeitet werden können, sind u. a.:

  • Dictionary<int, string>
  • WeekdaysEnum
  • List<DateTimeOffset>
  • DateTime
  • Int32

Das grundlegende Muster erstellt eine Klasse, die einen Typ verarbeiten kann. Das Factorymuster erstellt eine Klasse, die zur Laufzeit bestimmt, welcher spezifische Typ erforderlich ist, und erstellt dynamisch den entsprechenden Konverter.

Grundlegender Konverter – Beispiel

Das folgende Beispiel ist ein Konverter, der die Standardserialisierung für einen vorhandenen Datentyp außer Kraft setzt. Der Konverter verwendet das Format „mm/dd/yyyy“ für DateTimeOffset-Eigenschaften.

using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace SystemTextJsonSamples
{
    public class DateTimeOffsetJsonConverter : JsonConverter<DateTimeOffset>
    {
        public override DateTimeOffset Read(
            ref Utf8JsonReader reader,
            Type typeToConvert,
            JsonSerializerOptions options) =>
                DateTimeOffset.ParseExact(reader.GetString()!,
                    "MM/dd/yyyy", CultureInfo.InvariantCulture);

        public override void Write(
            Utf8JsonWriter writer,
            DateTimeOffset dateTimeValue,
            JsonSerializerOptions options) =>
                writer.WriteStringValue(dateTimeValue.ToString(
                    "MM/dd/yyyy", CultureInfo.InvariantCulture));
    }
}

Factorymusterkonverter – Beispiel

Im folgenden Code wird ein benutzerdefinierter Konverter gezeigt, der mit Dictionary<Enum,TValue> arbeitet. Der Code folgt dem Factorymuster, da der erste generische Typparameter Enum und der zweite offen ist. Die CanConvert-Methode gibt true nur für ein Dictionary mit zwei generischen Parametern zurück, wobei der erste ein Enum-Typ ist. Der innere Konverter ruft einen vorhandenen Konverter ab, um jeglichen Typ zu verarbeiten, der zur Laufzeit für TValue bereitgestellt wird.

using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace SystemTextJsonSamples
{
    public class DictionaryTKeyEnumTValueConverter : JsonConverterFactory
    {
        public override bool CanConvert(Type typeToConvert)
        {
            if (!typeToConvert.IsGenericType)
            {
                return false;
            }

            if (typeToConvert.GetGenericTypeDefinition() != typeof(Dictionary<,>))
            {
                return false;
            }

            return typeToConvert.GetGenericArguments()[0].IsEnum;
        }

        public override JsonConverter CreateConverter(
            Type type,
            JsonSerializerOptions options)
        {
            Type keyType = type.GetGenericArguments()[0];
            Type valueType = type.GetGenericArguments()[1];

            JsonConverter converter = (JsonConverter)Activator.CreateInstance(
                typeof(DictionaryEnumConverterInner<,>).MakeGenericType(
                    new Type[] { keyType, valueType }),
                BindingFlags.Instance | BindingFlags.Public,
                binder: null,
                args: new object[] { options },
                culture: null)!;

            return converter;
        }

        private class DictionaryEnumConverterInner<TKey, TValue> :
            JsonConverter<Dictionary<TKey, TValue>> where TKey : struct, Enum
        {
            private readonly JsonConverter<TValue> _valueConverter;
            private readonly Type _keyType;
            private readonly Type _valueType;

            public DictionaryEnumConverterInner(JsonSerializerOptions options)
            {
                // For performance, use the existing converter if available.
                _valueConverter = (JsonConverter<TValue>)options
                    .GetConverter(typeof(TValue));

                // Cache the key and value types.
                _keyType = typeof(TKey);
                _valueType = typeof(TValue);
            }

            public override Dictionary<TKey, TValue> Read(
                ref Utf8JsonReader reader,
                Type typeToConvert,
                JsonSerializerOptions options)
            {
                if (reader.TokenType != JsonTokenType.StartObject)
                {
                    throw new JsonException();
                }

                var dictionary = new Dictionary<TKey, TValue>();

                while (reader.Read())
                {
                    if (reader.TokenType == JsonTokenType.EndObject)
                    {
                        return dictionary;
                    }

                    // Get the key.
                    if (reader.TokenType != JsonTokenType.PropertyName)
                    {
                        throw new JsonException();
                    }

                    string? propertyName = reader.GetString();

                    // For performance, parse with ignoreCase:false first.
                    if (!Enum.TryParse(propertyName, ignoreCase: false, out TKey key) &&
                        !Enum.TryParse(propertyName, ignoreCase: true, out key))
                    {
                        throw new JsonException(
                            $"Unable to convert \"{propertyName}\" to Enum \"{_keyType}\".");
                    }

                    // Get the value.
                    TValue value;
                    if (_valueConverter != null)
                    {
                        reader.Read();
                        value = _valueConverter.Read(ref reader, _valueType, options)!;
                    }
                    else
                    {
                        value = JsonSerializer.Deserialize<TValue>(ref reader, options)!;
                    }

                    // Add to dictionary.
                    dictionary.Add(key, value);
                }

                throw new JsonException();
            }

            public override void Write(
                Utf8JsonWriter writer,
                Dictionary<TKey, TValue> dictionary,
                JsonSerializerOptions options)
            {
                writer.WriteStartObject();

                foreach ((TKey key, TValue value) in dictionary)
                {
                    var propertyName = key.ToString();
                    writer.WritePropertyName
                        (options.PropertyNamingPolicy?.ConvertName(propertyName) ?? propertyName);

                    if (_valueConverter != null)
                    {
                        _valueConverter.Write(writer, value, options);
                    }
                    else
                    {
                        JsonSerializer.Serialize(writer, value, options);
                    }
                }

                writer.WriteEndObject();
            }
        }
    }
}

Der vorangehende Code ist identisch mit dem, der weiter unten in diesem Artikel unter Unterstützung für Wörterbücher mit Schlüsseln, die keine Zeichenfolgen sind gezeigt wird.

Schritte zum Einhalten des grundlegenden Musters

Die folgenden Schritte erläutern, wie Sie einen Konverter erstellen, indem Sie dem grundlegenden Muster folgen:

  • Erstellen Sie eine Klasse, die von JsonConverter<T> abgeleitet ist, wobei T der Typ ist, der serialisiert und deserialisiert werden soll.
  • Setzen Sie die Read-Methode außer Kraft, um den eingehenden JSON-Code zu deserialisieren und in den Typ T zu konvertieren. Verwenden Sie den Utf8JsonReader, der an die Methode übergeben wird, um den JSON-Code zu lesen. Sie müssen sich nicht um die Behandlung von Teildaten kümmern, da der Serializer alle Daten für den aktuellen JSON-Bereich übergibt. Daher ist es nicht notwendig, diese Read Rückgabe truezu aufrufen Skip oder TrySkip zu überprüfen.
  • Setzen Sie die Write-Methode außer Kraft, um das eingehende Objekt vom Typ T zu serialisieren. Verwenden Sie den Utf8JsonWriter, der an die Methode übergeben wird, um den JSON-Code zu schreiben.
  • Setzen Sie die CanConvert-Methode nur außer Kraft, wenn dies notwendig ist. Die Standardimplementierung gibt true zurück, wenn der zu konvertierende Typ vom Typ T ist. Daher müssen Konverter, die nur den Typ T unterstützen, diese Methode nicht außer Kraft setzen. Ein Beispiel für einen Konverter, der diese Methode außer Kraft setzen muss, finden Sie im weiter unten in diesem Artikel im Abschnitt Polymorphe Deserialisierung.

Sie können den Quellcode des integrierten Konverters als Referenzimplementierungen zum Schreiben von benutzerdefinierten Konvertern verwenden.

Schritte zum Einhalten des Factorymusters

Die folgenden Schritte erläutern, wie Sie einen Konverter erstellen, indem Sie dem Factorymuster folgen:

  • Erstellen Sie eine von der JsonConverterFactory-Klasse abgeleitete Klasse.
  • Überschreiben Sie die CanConvert-Methode, um „true“ zurückzugeben, wenn der zu konvertierende Typ einer ist, den der Konverter verarbeiten kann. Wenn der Konverter z. B. für List<T> ist, kann er eventuell nur List<int>, List<string> und List<DateTime> verarbeiten.
  • Setzen Sie die CreateConverter-Methode außer Kraft, um eine Instanz einer Konverterklasse zurückzugeben, die den zur Laufzeit bereitgestellten zu konvertierenden Typ verarbeitet.
  • Erstellen Sie die Konverterklasse, die von der CreateConverter-Methode instanziiert wird.

Das Factorymuster ist für offene generische Typen erforderlich, da der Code, mit dem ein Objekt in eine und aus einer Zeichenfolge konvertiert werden soll, nicht für alle Typen identisch ist. Ein Konverter für einen offenen generischen Typ (z. B. List<T>) muss verdeckt einen Konverter für einen geschlossenen generischen Typ erstellen (z. B. List<DateTime>). Es muss Code geschrieben werden, um jeden geschlossenen generischen Typ zu verarbeiten, den der Konverter verarbeiten kann.

Der Typ Enum ähnelt einem offenen generischen Typ: Ein Konverter für Enum muss verdeckt einen Konverter für einen spezifischen Enum (z. B. WeekdaysEnum) erstellen.

Die Verwendung in Utf8JsonReader der Read Methode

Wenn Ihr Konverter ein JSON-Objekt konvertiert, wird die Utf8JsonReader Position beim Starten des Read Objekttokens auf dem Startobjekttoken positioniert. Anschließend müssen Sie alle Token in diesem Objekt lesen und die Methode mit dem Leser beenden, der auf dem entsprechenden Endobjekttoken positioniert ist. Wenn Sie über das Ende des Objekts hinaus lesen oder bevor Sie das entsprechende Endtoken erreichen, erhalten Sie eine JsonException Ausnahme, die folgendes angibt:

Der Konverter 'ConverterName' liest zu viel oder nicht genug.

Ein Beispiel finden Sie im vorherigen Musterbeispielkonverter des Werksmusters. Die Read Methode beginnt mit der Überprüfung, dass der Leser auf einem Startobjekttoken positioniert ist. Es liest, bis es findet, dass er auf dem nächsten Endobjekttoken positioniert ist. Es wird auf dem nächsten Endobjekttoken beendet, da es keine eingegriffenen Startobjekttoken gibt, die ein Objekt innerhalb des Objekts angeben würden. Die gleiche Regel zum Starttoken und Endtoken gilt, wenn Sie ein Array konvertieren. Ein Beispiel finden Sie Stack<T> weiter unten in diesem Artikel im Beispielkonverter.

Fehlerbehandlung

Das Serialisierungsmodul beinhaltet spezielle Verarbeitungsmethoden für die Ausnahmetypen JsonException und NotSupportedException.

JsonException

Wenn Sie die Ausnahme JsonException ohne Fehlermeldung auslösen, erstellt das Serialisierungsmodul automatisch eine Meldung, die den Pfad zu dem Teil des JSON-Codes enthält, der den Fehler verursacht hat. Beispielsweise erzeugt die Anweisung throw new JsonException() eine Fehlermeldung wie im folgenden Beispiel:

Unhandled exception. System.Text.Json.JsonException:
The JSON value could not be converted to System.Object.
Path: $.Date | LineNumber: 1 | BytePositionInLine: 37.

Wenn Sie eine Nachricht (zthrow new JsonException("Error occurred"). B. ) angeben, legt der Serializer weiterhin die PathEigenschaften und BytePositionInLineLineNumberEigenschaften fest.

NotSupportedException

Wenn Sie die Ausnahme NotSupportedException auslösen, sind in der Meldung immer die Pfadinformationen enthalten. Wenn Sie eine Meldung angeben, werden die Pfadinformationen an diese angefügt. Beispielsweise erzeugt die Anweisung throw new NotSupportedException("Error occurred.") eine Fehlermeldung wie im folgenden Beispiel:

Error occurred. The unsupported member type is located on type
'System.Collections.Generic.Dictionary`2[Samples.SummaryWords,System.Int32]'.
Path: $.TemperatureRanges | LineNumber: 4 | BytePositionInLine: 24

Auswahl des richtigen Ausnahmetyps

Wenn die JSON-Nutzdaten Token enthalten, die für den deserialisierten Typ nicht gültig sind, sollten Sie die Ausnahme JsonException auslösen.

Wenn bestimmte Typen nicht zugelassen werden sollen, lösen Sie die Ausnahme NotSupportedException aus. Diese Ausnahme löst das Serialisierungsmodul automatisch für nicht unterstützte Typen aus. Beispielsweise wird System.Type aus Sicherheitsgründen nicht unterstützt, sodass der Versuch, diesen Typ zu deserialisieren, zu einer NotSupportedException-Ausnahme führt.

Sie können bei Bedarf andere Ausnahmen auslösen. Diese enthalten jedoch nicht automatisch JSON-Pfadinformationen.

Registrieren eines benutzerdefinierten Konverters

Registrieren Sie einen benutzerdefinierten Konverter, damit die Methoden Serialize und Deserialize diesen verwenden. Wählen Sie einen der folgenden Ansätze aus:

  • Hinzufügen einer Instanz des Konverters zu der JsonSerializerOptions.Converters-Sammlung.
  • Anwenden des [JsonConverter]-Attributs auf die Eigenschaften, die den benutzerdefinierten Konverter benötigen.
  • Anwenden des [JsonConverter]-Attributs auf eine Klasse oder Struktur, die einen benutzerdefinierten Werttyp darstellt.

Registrierungsbeispiel – Converters-Sammlung

Im Folgenden finden Sie ein Beispiel, das das DateTimeOffsetJsonConverter zum Standard für Eigenschaften des Typs DateTimeOffsetmacht:

var serializeOptions = new JsonSerializerOptions
{
    WriteIndented = true,
    Converters =
    {
        new DateTimeOffsetJsonConverter()
    }
};

jsonString = JsonSerializer.Serialize(weatherForecast, serializeOptions);

Angenommen, Sie serialisieren eine Instanz des folgenden Typs:

public class WeatherForecast
{
    public DateTimeOffset Date { get; set; }
    public int TemperatureCelsius { get; set; }
    public string? Summary { get; set; }
}

Hier sehen Sie ein Beispiel für eine JSON-Ausgabe, die anzeigt, dass der benutzerdefinierte Konverter verwendet wurde:

{
  "Date": "08/01/2019",
  "TemperatureCelsius": 25,
  "Summary": "Hot"
}

Der folgende Code verwendet denselben Ansatz zum Deserialisieren mithilfe des benutzerdefinierten DateTimeOffset-Konverters:

var deserializeOptions = new JsonSerializerOptions();
deserializeOptions.Converters.Add(new DateTimeOffsetJsonConverter());
weatherForecast = JsonSerializer.Deserialize<WeatherForecast>(jsonString, deserializeOptions)!;

Registrierungsbeispiel – [JsonConverter]-Attribut für eine Eigenschaft

Im folgenden Code wird ein benutzerdefinierter Konverter für die Date-Eigenschaft ausgewählt:

public class WeatherForecastWithConverterAttribute
{
    [JsonConverter(typeof(DateTimeOffsetJsonConverter))]
    public DateTimeOffset Date { get; set; }
    public int TemperatureCelsius { get; set; }
    public string? Summary { get; set; }
}

Der Code zum Serialisieren von WeatherForecastWithConverterAttribute erfordert nicht die Verwendung von JsonSerializeOptions.Converters:

var serializeOptions = new JsonSerializerOptions
{
    WriteIndented = true
};
jsonString = JsonSerializer.Serialize(weatherForecast, serializeOptions);

Der Code zum Deserialisieren erfordert ebenfalls nicht die Verwendung von Converters:

weatherForecast = JsonSerializer.Deserialize<WeatherForecastWithConverterAttribute>(jsonString)!;

Registrierungsbeispiel – [JsonConverter]-Attribut für einen Typ

Der folgende Code erstellt eine Struktur und wendet das [JsonConverter]-Attribut darauf an:

using System.Text.Json.Serialization;

namespace SystemTextJsonSamples
{
    [JsonConverter(typeof(TemperatureConverter))]
    public struct Temperature
    {
        public Temperature(int degrees, bool celsius)
        {
            Degrees = degrees;
            IsCelsius = celsius;
        }

        public int Degrees { get; }
        public bool IsCelsius { get; }
        public bool IsFahrenheit => !IsCelsius;

        public override string ToString() =>
            $"{Degrees}{(IsCelsius ? "C" : "F")}";

        public static Temperature Parse(string input)
        {
            int degrees = int.Parse(input.Substring(0, input.Length - 1));
            bool celsius = input.Substring(input.Length - 1) == "C";

            return new Temperature(degrees, celsius);
        }
    }
}

Hier sehen Sie den benutzerdefinierten Konverter für die vorangehende Struktur:

using System.Text.Json;
using System.Text.Json.Serialization;

namespace SystemTextJsonSamples
{
    public class TemperatureConverter : JsonConverter<Temperature>
    {
        public override Temperature Read(
            ref Utf8JsonReader reader,
            Type typeToConvert,
            JsonSerializerOptions options) =>
                Temperature.Parse(reader.GetString()!);

        public override void Write(
            Utf8JsonWriter writer,
            Temperature temperature,
            JsonSerializerOptions options) =>
                writer.WriteStringValue(temperature.ToString());
    }
}

Das [JsonConverter]-Attribut der Struktur registriert den benutzerdefinierten Konverter als Standard für Eigenschaften vom Typ Temperature. Der Konverter wird automatisch für die TemperatureCelsius-Eigenschaft des folgenden Typs verwendet, wenn Sie sie serialisieren oder deserialisieren:

public class WeatherForecastWithTemperatureStruct
{
    public DateTimeOffset Date { get; set; }
    public Temperature TemperatureCelsius { get; set; }
    public string? Summary { get; set; }
}

Rangfolge für die Registrierung von Konvertern

Während der Serialisierung oder Deserialisierung wird für jedes JSON-Element ein Konverter in der folgenden Reihenfolge ausgewählt, wobei die Auflistung von der höchsten Priorität zur niedrigsten erfolgt:

  • Auf eine Eigenschaft angewendeter [JsonConverter].
  • Ein der Converters-Sammlung hinzugefügt er Konverter.
  • Auf einen benutzerdefinierten Werttyp oder ein POCO angewendeter [JsonConverter].

Wenn mehrere benutzerdefinierte Konverter für einen Typ in der Converters-Sammlung registriert sind, wird der erste Konverter verwendet, der für CanConvert „true“ zurückgibt.

Ein integrierter Konverter wird nur ausgewählt, wenn kein anwendbarer benutzerdefinierter Konverter registriert ist.

Konverterbeispiele für gängige Szenarien

In den folgenden Abschnitten werden Konverterbeispiele bereitgestellt, in denen einige gängige Szenarien behandelt werden, die von integrierten Funktionen nicht verarbeitet werden.

Informationen zu einem Beispielkonverter DataTable finden Sie unter Unterstützten Auflistungstypen.

Deserialisieren abgeleiteter Typen in Objekteigenschaften

Beim Deserialisieren in eine Eigenschaft vom Typ object wird ein JsonElement Objekt erstellt. Der Grund dafür ist, dass der Deserialisierer nicht weiß, welcher CLR-Typ erstellt werden soll, und nicht versucht, diesen vorherzusagen. Wenn z. B. eine JSON-Eigenschaft den Wert „true“ hat, leitet der Deserialisierer nicht ab, dass der Wert vom Typ Boolean ist, und wenn ein Element „01/01/2019“ aufweist, leitet der Deserialisierer nicht ab, dass es sich um einen DateTime-Typ handelt.

Typableitung kann ungenau sein. Wenn der Deserialisierer eine JSON-Zahl, die kein Dezimaltrennzeichen aufweist, als long analysiert, kann dies zu einem „außerhalb des zulässigen Bereichs“-Probleme führen, wenn der Wert ursprünglich als ulong oder BigInteger serialisiert wurde. Wird eine Zahl, die ein Dezimaltrennzeichen aufweist, als double analysiert, kann hierdurch die Genauigkeit verloren gehen, wenn die Zahl ursprünglich als decimal serialisiert wurde.

Für Szenarien, die eine Typableitung erfordern, zeigt der folgende Code einen benutzerdefinierten Konverter für object-Eigenschaften an. Der Code konvertiert Folgendes:

  • true un false in Boolean
  • Zahlen ohne Dezimaltrennzeichen in long
  • Zahlen mit Dezimaltrennzeichen in double
  • Datumsangaben in DateTime
  • Zeichenfolgen in string
  • Alles andere in JsonElement
using System.Text.Json;
using System.Text.Json.Serialization;

namespace CustomConverterInferredTypesToObject
{
    public class ObjectToInferredTypesConverter : JsonConverter<object>
    {
        public override object Read(
            ref Utf8JsonReader reader,
            Type typeToConvert,
            JsonSerializerOptions options) => reader.TokenType switch
            {
                JsonTokenType.True => true,
                JsonTokenType.False => false,
                JsonTokenType.Number when reader.TryGetInt64(out long l) => l,
                JsonTokenType.Number => reader.GetDouble(),
                JsonTokenType.String when reader.TryGetDateTime(out DateTime datetime) => datetime,
                JsonTokenType.String => reader.GetString()!,
                _ => JsonDocument.ParseValue(ref reader).RootElement.Clone()
            };

        public override void Write(
            Utf8JsonWriter writer,
            object objectToWrite,
            JsonSerializerOptions options) =>
            JsonSerializer.Serialize(writer, objectToWrite, objectToWrite.GetType(), options);
    }

    public class WeatherForecast
    {
        public object? Date { get; set; }
        public object? TemperatureCelsius { get; set; }
        public object? Summary { get; set; }
    }

    public class Program
    {
        public static void Main()
        {
            string jsonString = @"{
  ""Date"": ""2019-08-01T00:00:00-07:00"",
  ""TemperatureCelsius"": 25,
  ""Summary"": ""Hot""
}";

            WeatherForecast weatherForecast = JsonSerializer.Deserialize<WeatherForecast>(jsonString)!;
            Console.WriteLine($"Type of Date property   no converter = {weatherForecast.Date!.GetType()}");

            var options = new JsonSerializerOptions();
            options.WriteIndented = true;
            options.Converters.Add(new ObjectToInferredTypesConverter());
            weatherForecast = JsonSerializer.Deserialize<WeatherForecast>(jsonString, options)!;
            Console.WriteLine($"Type of Date property with converter = {weatherForecast.Date!.GetType()}");

            Console.WriteLine(JsonSerializer.Serialize(weatherForecast, options));
        }
    }
}

// Produces output like the following example:
//
//Type of Date property   no converter = System.Text.Json.JsonElement
//Type of Date property with converter = System.DateTime
//{
//  "Date": "2019-08-01T00:00:00-07:00",
//  "TemperatureCelsius": 25,
//  "Summary": "Hot"
//}

Das Beispiel zeigt den Konvertercode und eine WeatherForecast Klasse mit object Eigenschaften. Die Main Methode deserialisiert eine JSON-Zeichenfolge in eine WeatherForecast Instanz, zunächst ohne Verwendung des Konverters und dann mit dem Konverter. Die Konsolenausgabe zeigt, dass ohne den Konverter der Laufzeittyp für die Date Eigenschaft ist JsonElement; mit dem Konverter ist DateTimeder Laufzeittyp .

Der Ordner unit tests (Komponententests) im System.Text.Json.Serialization-Namespace enthält weitere Beispiele für benutzerdefinierte Konverter, die die Deserialisierung in object-Eigenschaften verarbeiten.

using System.Text.Json;
using System.Text.Json.Serialization;

namespace SystemTextJsonSamples
{
    public class ObjectToInferredTypesConverter
        : JsonConverter<object>
    {
        public override object Read(
            ref Utf8JsonReader reader,
            Type typeToConvert,
            JsonSerializerOptions options) => reader.TokenType switch
            {
                JsonTokenType.True => true,
                JsonTokenType.False => false,
                JsonTokenType.Number when reader.TryGetInt64(out long l) => l,
                JsonTokenType.Number => reader.GetDouble(),
                JsonTokenType.String when reader.TryGetDateTime(out DateTime datetime) => datetime,
                JsonTokenType.String => reader.GetString()!,
                _ => JsonDocument.ParseValue(ref reader).RootElement.Clone()
            };

        public override void Write(
            Utf8JsonWriter writer,
            object objectToWrite,
            JsonSerializerOptions options) =>
            throw new InvalidOperationException("Should not get here.");
    }
}

Der folgende Code registriert den Konverter:

var deserializeOptions = new JsonSerializerOptions
{
    Converters =
    {
        new ObjectToInferredTypesConverter()
    }
};

Hier ist ein Beispieltyp mit object-Eigenschaften:

public class WeatherForecastWithObjectProperties
{
    public object? Date { get; set; }
    public object? TemperatureCelsius { get; set; }
    public object? Summary { get; set; }
}

Das folgende Beispiel für zu deserialisierenden JSON-Code enthält Werte, die als DateTime, long und string deserialisiert werden:

{
  "Date": "2019-08-01T00:00:00-07:00",
  "TemperatureCelsius": 25,
  "Summary": "Hot",
}

Ohne den benutzerdefinierten Konverter fügt die Deserialisierung eine JsonElement in jede Eigenschaft ein.

Der Ordner unit tests (Komponententests) im System.Text.Json.Serialization-Namespace enthält weitere Beispiele für benutzerdefinierte Konverter, die die Deserialisierung in object-Eigenschaften verarbeiten.

Unterstützung für Wörterbücher mit Schlüsseln, die keine Zeichenfolgen sind

Die integrierte Unterstützung für Wörterbuchsammlungen ist für Dictionary<string, TValue>. Das heißt, der Schlüssel muss eine Zeichenfolge sein. Um ein Wörterbuch mit einem Schlüssel vom Typ „integer“ (Ganzzahl) oder einem anderen Typ zu unterstützen, ist ein benutzerdefinierter Konverter erforderlich.

Im folgenden Code wird ein benutzerdefinierter Konverter gezeigt, der mit Dictionary<Enum,TValue> arbeitet:

using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace SystemTextJsonSamples
{
    public class DictionaryTKeyEnumTValueConverter : JsonConverterFactory
    {
        public override bool CanConvert(Type typeToConvert)
        {
            if (!typeToConvert.IsGenericType)
            {
                return false;
            }

            if (typeToConvert.GetGenericTypeDefinition() != typeof(Dictionary<,>))
            {
                return false;
            }

            return typeToConvert.GetGenericArguments()[0].IsEnum;
        }

        public override JsonConverter CreateConverter(
            Type type,
            JsonSerializerOptions options)
        {
            Type keyType = type.GetGenericArguments()[0];
            Type valueType = type.GetGenericArguments()[1];

            JsonConverter converter = (JsonConverter)Activator.CreateInstance(
                typeof(DictionaryEnumConverterInner<,>).MakeGenericType(
                    new Type[] { keyType, valueType }),
                BindingFlags.Instance | BindingFlags.Public,
                binder: null,
                args: new object[] { options },
                culture: null)!;

            return converter;
        }

        private class DictionaryEnumConverterInner<TKey, TValue> :
            JsonConverter<Dictionary<TKey, TValue>> where TKey : struct, Enum
        {
            private readonly JsonConverter<TValue> _valueConverter;
            private readonly Type _keyType;
            private readonly Type _valueType;

            public DictionaryEnumConverterInner(JsonSerializerOptions options)
            {
                // For performance, use the existing converter if available.
                _valueConverter = (JsonConverter<TValue>)options
                    .GetConverter(typeof(TValue));

                // Cache the key and value types.
                _keyType = typeof(TKey);
                _valueType = typeof(TValue);
            }

            public override Dictionary<TKey, TValue> Read(
                ref Utf8JsonReader reader,
                Type typeToConvert,
                JsonSerializerOptions options)
            {
                if (reader.TokenType != JsonTokenType.StartObject)
                {
                    throw new JsonException();
                }

                var dictionary = new Dictionary<TKey, TValue>();

                while (reader.Read())
                {
                    if (reader.TokenType == JsonTokenType.EndObject)
                    {
                        return dictionary;
                    }

                    // Get the key.
                    if (reader.TokenType != JsonTokenType.PropertyName)
                    {
                        throw new JsonException();
                    }

                    string? propertyName = reader.GetString();

                    // For performance, parse with ignoreCase:false first.
                    if (!Enum.TryParse(propertyName, ignoreCase: false, out TKey key) &&
                        !Enum.TryParse(propertyName, ignoreCase: true, out key))
                    {
                        throw new JsonException(
                            $"Unable to convert \"{propertyName}\" to Enum \"{_keyType}\".");
                    }

                    // Get the value.
                    TValue value;
                    if (_valueConverter != null)
                    {
                        reader.Read();
                        value = _valueConverter.Read(ref reader, _valueType, options)!;
                    }
                    else
                    {
                        value = JsonSerializer.Deserialize<TValue>(ref reader, options)!;
                    }

                    // Add to dictionary.
                    dictionary.Add(key, value);
                }

                throw new JsonException();
            }

            public override void Write(
                Utf8JsonWriter writer,
                Dictionary<TKey, TValue> dictionary,
                JsonSerializerOptions options)
            {
                writer.WriteStartObject();

                foreach ((TKey key, TValue value) in dictionary)
                {
                    var propertyName = key.ToString();
                    writer.WritePropertyName
                        (options.PropertyNamingPolicy?.ConvertName(propertyName) ?? propertyName);

                    if (_valueConverter != null)
                    {
                        _valueConverter.Write(writer, value, options);
                    }
                    else
                    {
                        JsonSerializer.Serialize(writer, value, options);
                    }
                }

                writer.WriteEndObject();
            }
        }
    }
}

Der folgende Code registriert den Konverter:

var serializeOptions = new JsonSerializerOptions();
serializeOptions.Converters.Add(new DictionaryTKeyEnumTValueConverter());

Der Konverter kann die TemperatureRanges-Eigenschaft der folgenden Klasse serialisieren und deserialisieren, die die folgende Enum verwendet:

public class WeatherForecastWithEnumDictionary
{
    public DateTimeOffset Date { get; set; }
    public int TemperatureCelsius { get; set; }
    public string? Summary { get; set; }
    public Dictionary<SummaryWordsEnum, int>? TemperatureRanges { get; set; }
}

public enum SummaryWordsEnum
{
    Cold, Hot
}

Die JSON-Ausgabe der Serialisierung sieht wie im folgenden Beispiel aus:

{
  "Date": "2019-08-01T00:00:00-07:00",
  "TemperatureCelsius": 25,
  "Summary": "Hot",
  "TemperatureRanges": {
    "Cold": 20,
    "Hot": 40
  }
}

Der Ordner unit tests (Komponententests) im System.Text.Json.Serialization-Namespace enthält weitere Beispiele für benutzerdefinierte Konverter, die Wörterbücher mit Schlüsseln, die keine Zeichenfolgen sind, verarbeiten.

Unterstützung polymorpher Deserialisierung

Integrierte Funktionen bieten einen begrenzten Funktionsbereich der polymorphen Serialisierung, aber gar keine Unterstützung für Deserialisierung. Die Deserialisierung erfordert einen benutzerdefinierten Konverter.

Angenommen, Sie verfügen beispielsweise über eine abstrakte Basisklasse Person mit den abgeleiteten Klassen Employee und Customer. Polymorphe Deserialisierung bedeutet, dass Sie zur Entwurfszeit Person als Deserialisierungsziel angeben können, und dass die Objekte Customer und Employee im JSON-Code zur Laufzeit ordnungsgemäß deserialisiert werden. Während der Deserialisierung müssen Sie nach Hinweisen suchen, die den erforderlichen Typ in JSON identifizieren. Die Arten der verfügbaren Hinweise variieren in jedem Szenario. Beispielsweise kann eine Diskriminatoreigenschaft verfügbar sein, oder Sie müssen sich darauf verlassen, dass eine bestimmte Eigenschaft vorhanden oder nicht vorhanden ist. Das aktuelle Release von System.Text.Json stellt keine Attribute bereit, um anzugeben, wie polymorphe Deserialisierungsszenarien behandelt werden sollen, sodass benutzerdefinierte Konverter erforderlich sind.

Der folgende Code zeigt eine Basisklasse, zwei abgeleitete Klassen und einen benutzerdefinierten Konverter für diese. Der Konverter verwendet eine Diskriminatoreigenschaft, um die polymorphe Deserialisierung durchzuführen. Der Typdiskriminator befindet sich nicht in den Klassendefinitionen, wird aber während der Serialisierung erstellt und während der Deserialisierung gelesen.

Wichtig

Der Beispielcode erfordert JSON-Objektname/Wertpaare, um in der Reihenfolge zu bleiben, was keine Standardanforderung von JSON ist.

public class Person
{
    public string? Name { get; set; }
}

public class Customer : Person
{
    public decimal CreditLimit { get; set; }
}

public class Employee : Person
{
    public string? OfficeNumber { get; set; }
}
using System.Text.Json;
using System.Text.Json.Serialization;

namespace SystemTextJsonSamples
{
    public class PersonConverterWithTypeDiscriminator : JsonConverter<Person>
    {
        enum TypeDiscriminator
        {
            Customer = 1,
            Employee = 2
        }

        public override bool CanConvert(Type typeToConvert) =>
            typeof(Person).IsAssignableFrom(typeToConvert);

        public override Person Read(
            ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            if (reader.TokenType != JsonTokenType.StartObject)
            {
                throw new JsonException();
            }

            reader.Read();
            if (reader.TokenType != JsonTokenType.PropertyName)
            {
                throw new JsonException();
            }

            string? propertyName = reader.GetString();
            if (propertyName != "TypeDiscriminator")
            {
                throw new JsonException();
            }

            reader.Read();
            if (reader.TokenType != JsonTokenType.Number)
            {
                throw new JsonException();
            }

            TypeDiscriminator typeDiscriminator = (TypeDiscriminator)reader.GetInt32();
            Person person = typeDiscriminator switch
            {
                TypeDiscriminator.Customer => new Customer(),
                TypeDiscriminator.Employee => new Employee(),
                _ => throw new JsonException()
            };

            while (reader.Read())
            {
                if (reader.TokenType == JsonTokenType.EndObject)
                {
                    return person;
                }

                if (reader.TokenType == JsonTokenType.PropertyName)
                {
                    propertyName = reader.GetString();
                    reader.Read();
                    switch (propertyName)
                    {
                        case "CreditLimit":
                            decimal creditLimit = reader.GetDecimal();
                            ((Customer)person).CreditLimit = creditLimit;
                            break;
                        case "OfficeNumber":
                            string? officeNumber = reader.GetString();
                            ((Employee)person).OfficeNumber = officeNumber;
                            break;
                        case "Name":
                            string? name = reader.GetString();
                            person.Name = name;
                            break;
                    }
                }
            }

            throw new JsonException();
        }

        public override void Write(
            Utf8JsonWriter writer, Person person, JsonSerializerOptions options)
        {
            writer.WriteStartObject();

            if (person is Customer customer)
            {
                writer.WriteNumber("TypeDiscriminator", (int)TypeDiscriminator.Customer);
                writer.WriteNumber("CreditLimit", customer.CreditLimit);
            }
            else if (person is Employee employee)
            {
                writer.WriteNumber("TypeDiscriminator", (int)TypeDiscriminator.Employee);
                writer.WriteString("OfficeNumber", employee.OfficeNumber);
            }

            writer.WriteString("Name", person.Name);

            writer.WriteEndObject();
        }
    }
}

Der folgende Code registriert den Konverter:

var serializeOptions = new JsonSerializerOptions();
serializeOptions.Converters.Add(new PersonConverterWithTypeDiscriminator());

Das JSON kann mit demselben Konverter deserialisiert werde, mit dem es auch serialisiert wurde, z. B.:

[
  {
    "TypeDiscriminator": 1,
    "CreditLimit": 10000,
    "Name": "John"
  },
  {
    "TypeDiscriminator": 2,
    "OfficeNumber": "555-1234",
    "Name": "Nancy"
  }
]

Mit dem Konvertercode im vorherigen Beispiel wird jede Eigenschaft manuell gelesen und geschrieben. Eine Alternative besteht darin, Deserialize oder Serialize aufzurufen, um einen Teil der Arbeit zu erledigen. Ein Beispiel hierzu finden Sie in diesem StackOverflow-Beitrag.

Eine alternative Möglichkeit, polymorphe Deserialisierung zu tun

Sie können die Read Methode aufrufenDeserialize:

  • Erstellen Sie einen Klon der Utf8JsonReader Instanz. Da Utf8JsonReader es sich um eine Struktur handelt, erfordert dies nur eine Zuordnungsanweisung.
  • Verwenden Sie den Klon, um die Diskriminatortoken zu lesen.
  • Rufen Sie Deserialize die ursprüngliche Reader Instanz auf, sobald Sie den benötigten Typ kennen. Sie können aufrufen Deserialize , da die ursprüngliche Reader Instanz weiterhin positioniert ist, um das Startobjekttoken zu lesen.

Ein Nachteil dieser Methode ist, dass Sie in der ursprünglichen Optionsinstanz, die den Konverter Deserializeregistriert, nicht übergeben werden können. Dies würde dazu führen, dass ein Stapelüberlauf ausgeführt wird, wie in den erforderlichen Eigenschaften erläutert. Das folgende Beispiel zeigt eine Read Methode, die diese Alternative verwendet:

public override Person Read(
    ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
    Utf8JsonReader readerClone = reader;

    if (readerClone.TokenType != JsonTokenType.StartObject)
    {
        throw new JsonException();
    }

    readerClone.Read();
    if (readerClone.TokenType != JsonTokenType.PropertyName)
    {
        throw new JsonException();
    }

    string? propertyName = readerClone.GetString();
    if (propertyName != "TypeDiscriminator")
    {
        throw new JsonException();
    }

    readerClone.Read();
    if (readerClone.TokenType != JsonTokenType.Number)
    {
        throw new JsonException();
    }

    TypeDiscriminator typeDiscriminator = (TypeDiscriminator)readerClone.GetInt32();
    Person person = typeDiscriminator switch
    {
        TypeDiscriminator.Customer => JsonSerializer.Deserialize<Customer>(ref reader)!,
        TypeDiscriminator.Employee => JsonSerializer.Deserialize<Employee>(ref reader)!,
        _ => throw new JsonException()
    };
    return person;
}

Support Roundtrip für StackT<>

Wenn Sie eine JSON-Zeichenfolge in ein Stack<T>-Objekt deserialisieren und dieses Objekt anschließend serialisieren, ist die Reihenfolge des Stapelinhalts umgekehrt. Dieses Verhalten gilt für die folgenden Typen und Schnittstellen sowie für benutzerdefinierte Typen, die von ihnen abgeleitet werden:

Um die Serialisierung und Deserialisierung zu unterstützen, die die ursprüngliche Reihenfolge im Stapel beibehält, ist ein benutzerdefinierter Konverter erforderlich.

Der folgende Code zeigt einen benutzerdefinierten Konverter, der Roundtrips zu und von Stack<T>-Objekten ermöglicht:

using System.Diagnostics;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace SystemTextJsonSamples
{
    public class JsonConverterFactoryForStackOfT : JsonConverterFactory
    {
        public override bool CanConvert(Type typeToConvert)
            => typeToConvert.IsGenericType
            && typeToConvert.GetGenericTypeDefinition() == typeof(Stack<>);

        public override JsonConverter CreateConverter(
            Type typeToConvert, JsonSerializerOptions options)
        {
            Debug.Assert(typeToConvert.IsGenericType &&
                typeToConvert.GetGenericTypeDefinition() == typeof(Stack<>));

            Type elementType = typeToConvert.GetGenericArguments()[0];

            JsonConverter converter = (JsonConverter)Activator.CreateInstance(
                typeof(JsonConverterForStackOfT<>)
                    .MakeGenericType(new Type[] { elementType }),
                BindingFlags.Instance | BindingFlags.Public,
                binder: null,
                args: null,
                culture: null)!;

            return converter;
        }
    }

    public class JsonConverterForStackOfT<T> : JsonConverter<Stack<T>>
    {
        public override Stack<T> Read(
            ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            if (reader.TokenType != JsonTokenType.StartArray)
            {
                throw new JsonException();
            }
            reader.Read();

            var elements = new Stack<T>();

            while (reader.TokenType != JsonTokenType.EndArray)
            {
                elements.Push(JsonSerializer.Deserialize<T>(ref reader, options)!);

                reader.Read();
            }

            return elements;
        }

        public override void Write(
            Utf8JsonWriter writer, Stack<T> value, JsonSerializerOptions options)
        {
            writer.WriteStartArray();

            var reversed = new Stack<T>(value);

            foreach (T item in reversed)
            {
                JsonSerializer.Serialize(writer, item, options);
            }

            writer.WriteEndArray();
        }
    }
}

Der folgende Code registriert den Konverter:

var options = new JsonSerializerOptions
{
    Converters = { new JsonConverterFactoryForStackOfT() },
};

Unterstützung der Aufzählung des Zeichenfolgenwerts

Standardmäßig kann das integrierte JsonStringEnumConverter Element Zeichenfolgenwerte für Enumerationen serialisieren und deserialisieren. Es funktioniert ohne eine angegebene Benennungsrichtlinie oder mit der Benennungsrichtlinie CamelCase . Es unterstützt keine anderen Benennungsrichtlinien, z. B. Schlangenfall. Informationen zu benutzerdefiniertem Konvertercode, der Roundtripping zu und aus Enumerationszeichenfolgenwerten während der Verwendung einer Schlangenfallbenennungsrichtlinie unterstützen kann, finden Sie unter GitHub Problem dotnet/runtime #31619.

Verwenden des Standardsystemkonverters

In einigen Szenarien möchten Sie möglicherweise den Standardsystemkonverter in einem benutzerdefinierten Konverter verwenden. Rufen Sie dazu den Systemkonverter aus der JsonSerializerOptions.Default Eigenschaft ab, wie im folgenden Beispiel gezeigt:

public class MyCustomConverter : JsonConverter<int>
{
    private readonly static JsonConverter<int> s_defaultConverter = 
        (JsonConverter<int>)JsonSerializerOptions.Default.GetConverter(typeof(int));

    // Custom serialization logic
    public override void Write(
        Utf8JsonWriter writer, int value, JsonSerializerOptions options)
    {
        writer.WriteStringValue(value.ToString());
    }

    // Fall back to default deserialization logic
    public override int Read(
        ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        return s_defaultConverter.Read(ref reader, typeToConvert, options);
    }
}

Behandeln von NULL-Werten

Standardmäßig verarbeitet das Serialisierungsmodul NULL-Werte wie folgt:

  • Für Verweis- und Nullable<T>-Typen:

    • null wird bei der Serialisierung nicht an benutzerdefinierte Konverter übergeben.
    • JsonTokenType.Null wird bei der Deserialisierung nicht an benutzerdefinierte Konverter übergeben.
    • Bei der Deserialisierung wird eine null-Instanz zurückgegeben.
    • null wird bei der Serialisierung direkt mit dem Writer geschrieben.
  • Für nicht auf NULL festlegbare Werttypen:

    • JsonTokenType.Null wird bei der Deserialisierung an benutzerdefinierte Konverter übergeben. (Wenn kein benutzerdefinierter Konverter verfügbar ist, wird vom internen Konverter für den Typ die Ausnahme JsonException ausgelöst.)

Dieses Verhalten für die Behandlung von NULL-Werten dient hauptsächlich zum Optimieren der Leistung, indem ein zusätzlicher Rückruf an den Konverter übersprungen wird. Darüber hinaus wird vermieden, dass Konverter für auf NULL festlegbare Typen gezwungen werden, am Anfang jeder Überschreibung der Methoden Read und Write nach null zu suchen.

Um einem benutzerdefinierten Konverter zu ermöglichen, null für einen Verweis- oder Werttyp zu verarbeiten, überschreiben Sie JsonConverter<T>.HandleNull so, dass true zurückgegeben wird, wie im folgenden Beispiel gezeigt:

using System.Text.Json;
using System.Text.Json.Serialization;

namespace CustomConverterHandleNull
{
    public class Point
    {
        public int X { get; set; }
        public int Y { get; set; }

        [JsonConverter(typeof(DescriptionConverter))]
        public string? Description { get; set; }
    }

    public class DescriptionConverter : JsonConverter<string>
    {
        public override bool HandleNull => true;

        public override string Read(
            ref Utf8JsonReader reader,
            Type typeToConvert,
            JsonSerializerOptions options) =>
            reader.GetString() ?? "No description provided.";

        public override void Write(
            Utf8JsonWriter writer,
            string value,
            JsonSerializerOptions options) =>
            writer.WriteStringValue(value);
    }

    public class Program
    {
        public static void Main()
        {
            string json = @"{""x"":1,""y"":2,""Description"":null}";

            Point point = JsonSerializer.Deserialize<Point>(json)!;
            Console.WriteLine($"Description: {point.Description}");
        }
    }
}

// Produces output like the following example:
//
//Description: No description provided.

Beibehalten von Verweisen

Standardmäßig werden Referenzdaten nur für jeden Aufruf oder Deserializejeden Aufruf Serialize zwischengespeichert. Um Verweise von einem Aufruf auf einen Serialize/Deserialize anderen beizubehalten, stamme die ReferenceResolver Instanz auf der Anrufwebsite von .Serialize/Deserialize Der folgende Code zeigt ein Beispiel für dieses Szenario:

  • Sie schreiben einen benutzerdefinierten Konverter für den Company Typ.
  • Sie möchten die Supervisor Eigenschaft nicht manuell serialisieren, was eine Employee. Sie möchten dies an den Serializer delegieren und auch die Verweise beibehalten, die Sie bereits gespeichert haben.

Dies sind die Employee folgenden Company Klassen:

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

public class Company
{
    public string? Name { get; set; }
    public Employee? Supervisor { get; set; }
}

Der Konverter sieht wie folgt aus:

class CompanyConverter : JsonConverter<Company>
{
    public override Company Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        throw new NotImplementedException();
    }

    public override void Write(Utf8JsonWriter writer, Company value, JsonSerializerOptions options)
    {
        writer.WriteStartObject();

        writer.WriteString("Name", value.Name);

        writer.WritePropertyName("Supervisor");
        JsonSerializer.Serialize(writer, value.Supervisor, options);

        writer.WriteEndObject();
    }
}

Eine Klasse, die von ReferenceResolver den Verweisen in einem Wörterbuch abgeleitet wird:

class MyReferenceResolver : ReferenceResolver
{
    private uint _referenceCount;
    private readonly Dictionary<string, object> _referenceIdToObjectMap = new ();
    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;
    }
}

Eine Klasse, die von ReferenceHandler einer Instanz MyReferenceResolver abgeleitet wird, und erstellt nur bei Bedarf eine neue Instanz (in einer in diesem Beispiel benannten Reset Methode):

class MyReferenceHandler : ReferenceHandler
{
    public MyReferenceHandler() => Reset();

    private ReferenceResolver? _rootedResolver;
    public override ReferenceResolver CreateResolver() => _rootedResolver!;
    public void Reset() => _rootedResolver = new MyReferenceResolver();

}

Wenn der Beispielcode den Serializer aufruft, wird eine JsonSerializerOptions Instanz verwendet, in der die ReferenceHandler Eigenschaft auf eine Instanz von MyReferenceHandler. Wenn Sie diesem Muster folgen, stellen Sie sicher, dass Sie das ReferenceResolver Wörterbuch zurücksetzen, wenn Sie die Serialisierung abgeschlossen haben, um es für immer zu vergrößern.

var options = new JsonSerializerOptions();

options.Converters.Add(new CompanyConverter());
var myReferenceHandler = new MyReferenceHandler();
options.ReferenceHandler = myReferenceHandler;
options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
options.WriteIndented = true;

string str = JsonSerializer.Serialize(tyler, options);

// Reset after serializing to avoid out of bounds memory growth in the resolver.
myReferenceHandler.Reset();

Im vorherigen Beispiel wird nur serialisiert, aber ein ähnlicher Ansatz kann für die Deserialisierung übernommen werden.

Informationen zum Beibehalten von Verweisen finden Sie in der .NET 5-Version dieser Seite.

Weitere Beispiele für benutzerdefinierte Konverter

Im Artikel Migrieren von Newtonsoft.Json zu System.Text.Json finden Sie zusätzliche Beispiele für benutzerdefinierte Konverter.

Der Ordner unit tests (Komponententests) im System.Text.Json.Serialization-Quellcode enthält weitere Beispiele für benutzerdefinierte Konverter, wie z. B.:

Wenn Sie einen Konverter erstellen müssen, der das Verhalten eines vorhandenen integrierten Konverters ändert, können Sie den Quellcode des vorhandenen Konverters abrufen, um diesen als Ausgangspunkt für die Anpassung zu verwenden.

Zusätzliche Ressourcen