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 neuen Funktionen anzupassen oder zu erweitern. Folgende Szenarien werden später in diesem Artikel abgedeckt:

Visual Basic kann nicht zum Schreiben benutzerdefinierter Konverter verwendet werden, kann aber Konverter aufrufen, die in C#-Bibliotheken implementiert sind. Weitere Informationen finden Sie unter Visual Basic-Unterstützung.

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[] typeArguments = type.GetGenericArguments();
            Type keyType = typeArguments[0];
            Type valueType = typeArguments[1];

            JsonConverter converter = (JsonConverter)Activator.CreateInstance(
                typeof(DictionaryEnumConverterInner<,>).MakeGenericType(
                    [keyType, valueType]),
                BindingFlags.Instance | BindingFlags.Public,
                binder: null,
                args: [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.
                _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.
                    reader.Read();
                    TValue value = _valueConverter.Read(ref reader, _valueType, 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)
                {
                    string propertyName = key.ToString();
                    writer.WritePropertyName
                        (options.PropertyNamingPolicy?.ConvertName(propertyName) ?? propertyName);

                    _valueConverter.Write(writer, value, options);
                }

                writer.WriteEndObject();
            }
        }
    }
}

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 keine Gedanken über die Behandlung von Teildaten machen, da der Serialisierer alle Daten für den aktuellen JSON-Bereich übergibt. Es ist also nicht erforderlich, Skip oder TrySkip aufzurufen oder zu überprüfen, ob von Readtrue zurückgegeben wird.
  • 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 von Utf8JsonReader in der Read-Methode

Wenn Ihr Konverter ein JSON-Objekt konvertiert, wird der Utf8JsonReader auf dem Startobjekttoken positioniert, wenn die Read-Methode beginnt. Anschließend müssen Sie alle Token in diesem Objekt lesen und die Methode beenden, während der Reader auf dem entsprechenden Endobjekttoken positioniert ist. Wenn Sie über das Ende des Objekts hinaus lesen oder beenden, bevor das entsprechende Endtoken erreicht wurde, erhalten Sie eine JsonException-Ausnahme, die Folgendes angibt:

Der Konverter „ConverterName“ hat zu viel oder nicht genug gelesen.

Ein Beispiel finden Sie im vorstehenden Factorymuster-Beispielkonverter. Die Read-Methode überprüft zunächst, ob der Reader auf einem Startobjekttoken positioniert ist. Sie liest, bis sie feststellt, dass sie auf dem nächsten Endobjekttoken positioniert ist. Sie wird auf dem nächsten Endobjekttoken beendet, da es keine intervenierenden Startobjekttoken gibt, die ein Objekt innerhalb des Objekts angeben würden. Die gleiche Regel für Starttoken und Endtoken gilt beim Konvertieren von Arrays. Eine Beispiel finden Sie unter dem Stack<T>-Beispielkonverter weiter unten in diesem Artikel.

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 Meldung angeben (z. B. throw new JsonException("Error occurred")), legt das Serialisierungsmodul die Eigenschaften Path, LineNumber und BytePositionInLine trotzdem 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, durch das der DateTimeOffsetJsonConverter zum Standard für Eigenschaften vom Typ DateTimeOffset gemacht wird:

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 CanConverttrue 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.

Einen DataTable-Beispielkonverter finden Sie unter Unterstützte Sammlungstypen.

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 den Konverter und dann mit dem Konverter. Die Konsolenausgabe zeigt, dass ohne den Konverter der Laufzeittyp für die Date-Eigenschaft JsonElement ist. Mit dem Konverter ist der Laufzeittyp DateTime.

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 polymorpher Deserialisierung

.NET 7 bietet Unterstützung für sowohl polymorphe Serialisierung als auch Deserialisierung. In früheren .NET-Versionen gab es jedoch nur eine begrenzte Unterstützung für polymorphe Serialisierung und keine Unterstützung für die Deserialisierung. Wenn Sie .NET 6 oder eine frühere Version verwenden, erfordert die Deserialisierung 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

Für den Beispielcode ist es erforderlich, dass JSON-Objektnamen-Wert-Paare in der Reihenfolge 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 zur polymorphen Deserialisierung

Sie können Deserialize in der Read-Methode aufrufen:

  • Erstellen Sie einen Klon der Utf8JsonReader-Instanz. Da Utf8JsonReader eine Struktur ist, erfordert dies nur eine Zuordnungsanweisung.
  • Verwenden Sie den Klon, um alle Diskriminatortoken zu lesen.
  • Rufen Sie Deserialize mit der ursprünglichen Reader-Instanz auf, sobald Sie den benötigten Typ kennen. Sie können Deserialize aufrufen, da die ursprüngliche Reader-Instanz nach wie vor für das Lesen des Startobjekttokens positioniert ist.

Ein Nachteil dieser Methode ist, dass Sie die ursprüngliche Optionsinstanz nicht übergeben können, die den Konverter bei Deserialize registriert. Dies würde zu einem Stapelüberlauf führen, wie unter Erforderliche 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;
}

Supportroundtrip für Stack-Typen

Wenn Sie eine JSON-Zeichenfolge in ein Stack-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() },
};

Benennungsrichtlinien für die Enumerationszeichenfolgenentialisierung

Standardmäßig kann der integrierte JsonStringEnumConverter Zeichenfolgenwerte für Enumerationen serialisieren und deserialisieren. Dies funktioniert ohne Angabe einer Benennungsrichtlinie oder mit der CamelCase-Benennungsrichtlinie. Andere Benennungsrichtlinien, z. B. Snake Case, werden nicht unterstützt. Informationen zu benutzerdefiniertem Konvertercode, der Roundtrips zu und von Enumerations-Zeichenfolgenwerten auch mit einer Snake Case-Benennungsrichtlinie unterstützen kann, finden Sie unter GitHub-Issue dotnet/runtime #31619. Alternativ können Sie ein Upgrade auf .NET 7 oder höhere Versionen durchführen, die integrierte Unterstützung für das Anwenden von Benennungsrichtlinien beim Roundtrip auf und von Enumerationszeichenfolgenwerten bieten.

Verwenden des Standardsystemkonverters

In manchen Szenarien kann es sinnvoll sein, den Standardsystemkonverter in einem benutzerdefinierten Konverter zu verwenden. Rufen Sie hierzu 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 Verweisdaten nur für jeden Aufruf von Serialize oder Deserializezwischengespeichert. Zum Beibehalten von Verweisen von einem Serialize/Deserialize-Aufruf zum nächsten rooten Sie die ReferenceResolver-Instanz auf der Aufrufwebsite von Serialize/Deserialize. Der folgende Code zeigt ein Beispiel für ein solches Szenario:

  • Sie schreiben einen benutzerdefinierten Konverter für den Typ Company.
  • Sie möchten die Supervisor-Eigenschaft nicht manuell serialisieren, bei der es sich um einen Employee handelt. Sie möchten dies an das Serialisierungsprogramm delegieren und außerdem die bereits gespeicherten Verweise beibehalten.

Dies sind die Klassen Employee und Company:

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 abgeleitet wird, speichert die Verweise in einem Wörterbuch:

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 abgeleitet wird, enthält eine Instanz von MyReferenceResolver und erstellt nur bei Bedarf eine neue Instanz (in diesem Beispiel in einer Methode namens Reset):

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

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

}

Wenn der Beispielcode das Serialisierungsprogramm aufruft, wird eine JsonSerializerOptions-Instanz verwendet, in der die ReferenceHandler-Eigenschaft auf eine Instanz von MyReferenceHandlerfestgelegt ist. Wenn Sie diesem Muster folgen, achten Sie unbedingt darauf, das ReferenceResolver-Wörterbuch zurückzusetzen, wenn Sie die Serialisierung abgeschlossen haben, damit es nicht ständig weiter anwächst.

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 für die Deserialisierung kann ein ähnlicher Ansatz verwendet werden.

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