Anpassen eines JSON-Vertrags

Die System.Text.Json-Bibliothek erstellt einen JSON-Vertrag für jeden .NET-Typ, der definiert, wie der Typ serialisiert und deserialisiert werden soll. Der Vertrag wird von der Form des Typs abgeleitet, die Merkmale wie Eigenschaften und Felder sowie die Implementierung der IEnumerable- oder IDictionary-Schnittstelle enthält. Typen werden Verträgen entweder zur Laufzeit mithilfe der Reflexion oder zur Kompilierzeit mithilfe des Quellgenerators zugeordnet.

Ab .NET 7 können Sie diese JSON-Verträge anpassen, um mehr Kontrolle darüber zu erhalten, wie Typen in JSON konvertiert werden und umgekehrt. Die folgende Liste enthält nur einige Beispiele für die Arten von Anpassungen, die Sie an der Serialisierung und Deserialisierung vornehmen können:

  • Serialisieren von privaten Feldern und Eigenschaften.
  • Unterstützung mehrerer Namen für eine einzelne Eigenschaft (z. B. wenn eine frühere Bibliotheksversion einen anderen Namen verwendet hat).
  • Ignorieren von Eigenschaften mit einem bestimmten Namen, Typ oder Wert.
  • Unterscheiden zwischen expliziten null-Werten und dem Fehlen eines Werts in der JSON-Nutzlast.
  • Unterstützen Sie System.Runtime.Serialization-Attribute, wie z. B. DataContractAttribute. Weitere Informationen finden Sie unter System.Runtime.Serialization-Attribute.
  • Lösen Sie eine Ausnahme aus, wenn der JSON-Code eine Eigenschaft enthält, die nicht Teil des Zieltyps ist. Weitere Informationen finden Sie unter Behandeln fehlender Member.

Vorgehensweise

Es gibt zwei Möglichkeiten, die Anpassung durchzuführen. Für beide müssen Sie einen Resolver abrufen, der eine JsonTypeInfo-Instanz für jeden Typ bereitstellt, der serialisiert werden muss.

  • Sie rufen den DefaultJsonTypeInfoResolver()-Konstruktor auf, um den JsonSerializerOptions.TypeInfoResolver abzurufen, und fügen Ihre benutzerdefinierten Aktionen der Modifiers-Eigenschaft hinzu.

    Beispiel:

    JsonSerializerOptions options = new()
    {
        TypeInfoResolver = new DefaultJsonTypeInfoResolver
        {
            Modifiers =
            {
                MyCustomModifier1,
                MyCustomModifier2
            }
        }
    };
    

    Wenn Sie mehrere Modifizierer hinzufügen, werden diese sequenziell aufgerufen.

  • Durch Schreiben eines benutzerdefinierten Resolvers, der IJsonTypeInfoResolver implementiert.

    • Wenn ein Typ nicht behandelt wird, sollte IJsonTypeInfoResolver.GetTypeInfo für diesen Typ null zurückgeben.
    • Sie können Ihren benutzerdefinierten Resolver auch mit anderen kombinieren, z. B. mit dem Standardresolver. Die Resolver werden der Reihe nach abgefragt, bis ein JsonTypeInfo-Wert ungleich NULL für den Typ zurückgegeben wird.

Konfigurierbare Aspekte

Die JsonTypeInfo.Kind-Eigenschaft gibt an, wie der Konverter einen bestimmten Typ serialisiert, z. B. als Objekt oder Array, und ob seine Eigenschaften serialisiert werden. Sie können diese Eigenschaft abfragen, um zu bestimmen, welche Aspekte des JSON-Vertrags eines Typs Sie konfigurieren können. Es gibt vier verschiedene Arten:

JsonTypeInfo.Kind BESCHREIBUNG
JsonTypeInfoKind.Object Der Konverter serialisiert den Typ in ein JSON-Objekt und verwendet dessen Eigenschaften. Diese Art wird für die meisten Klassen- und Strukturtypen verwendet und bietet die größte Flexibilität.
JsonTypeInfoKind.Enumerable Der Konverter serialisiert den Typ in ein JSON-Array. Diese Art wird für Typen wie List<T> und Array verwendet.
JsonTypeInfoKind.Dictionary Der Konverter serialisiert den Typ in ein JSN-Objekt. Diese Art wird für Typen wie Dictionary<K, V> verwendet.
JsonTypeInfoKind.None Der Konverter gibt nicht an, wie er den Typ serialisiert, oder welche JsonTypeInfo-Eigenschaften verwendet werden sollen. Diese Art wird für Typen wie System.Object, int und string sowie alle Typen verwendet, die einen benutzerdefinierten Konverter verwenden.

Zusatztasten

Ein Modifizierer ist eine Action<JsonTypeInfo> oder eine Methode mit einem JsonTypeInfo-Parameter, der den aktuellen Zustand des Vertrags als Argument abruft und Änderungen am Vertrag vornimmt. Beispielsweise können Sie die vorab aufgefüllten Eigenschaften für die angegebene JsonTypeInfo durchlaufen, um die gewünschte Eigenschaft zu finden, und dann deren JsonPropertyInfo.Get-Eigenschaft (für die Serialisierung) oder JsonPropertyInfo.Set-Eigenschaft (für die Deserialisierung) ändern. Alternativ können Sie mit JsonTypeInfo.CreateJsonPropertyInfo(Type, String) eine neue Eigenschaft erstellen und der JsonTypeInfo.Properties-Sammlung hinzufügen.

Die folgende Tabelle zeigt die Änderungen, die Sie vornehmen können, und wie Sie sie erreichen können.

Modifikation (Modification) Anwendbare JsonTypeInfo.Kind Erforderliche Schritte Beispiel
Anpassen des Werts einer Eigenschaft JsonTypeInfoKind.Object Ändern Sie den JsonPropertyInfo.Get-Delegaten (für die Serialisierung) oder den JsonPropertyInfo.Set-Delegaten (zur Deserialisierung) für die Eigenschaft. Schrittweises Erhöhen eines Eigenschaftswerts
Hinzufügen oder Entfernen von Eigenschaften JsonTypeInfoKind.Object Fügen Sie der JsonTypeInfo.Properties-Liste Elemente hinzu, oder entfernen Sie sie daraus. Serialisieren privater Felder
Bedingtes Serialisieren einer Eigenschaft JsonTypeInfoKind.Object Ändern Sie das JsonPropertyInfo.ShouldSerialize-Prädikat der Eigenschaft. Ignorieren von Eigenschaften eines bestimmten Typs
Anpassen der Zahlenbehandlung für einen bestimmten Typ JsonTypeInfoKind.None Ändern Sie den JsonTypeInfo.NumberHandling-Wert für den Typ. Zulassen, dass int-Werte Zeichenfolgen sind

Beispiel: Schrittweises Erhöhen eines Eigenschaftswerts

Betrachten Sie das folgende Beispiel, in dem der Modifizierer den Wert einer bestimmten Eigenschaft bei der Deserialisierung erhöht, indem er seinen JsonPropertyInfo.Set-Delegaten ändert. Neben dem Definieren des Modifizierers wird im Beispiel auch ein neues Attribut eingeführt, das verwendet wird, um die Eigenschaft zu suchen, deren Wert erhöht werden soll. Dies ist ein Beispiel für das Anpassen einer Eigenschaft.

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

namespace Serialization
{
    // Custom attribute to annotate the property
    // we want to be incremented.
    [AttributeUsage(AttributeTargets.Property)]
    class SerializationCountAttribute : Attribute
    {
    }

    // Example type to serialize and deserialize.
    class Product
    {
        public string Name { get; set; } = "";
        [SerializationCount]
        public int RoundTrips { get; set; }
    }

    public class SerializationCountExample
    {
        // Custom modifier that increments the value
        // of a specific property on deserialization.
        static void IncrementCounterModifier(JsonTypeInfo typeInfo)
        {
            foreach (JsonPropertyInfo propertyInfo in typeInfo.Properties)
            {
                if (propertyInfo.PropertyType != typeof(int))
                    continue;

                object[] serializationCountAttributes = propertyInfo.AttributeProvider?.GetCustomAttributes(typeof(SerializationCountAttribute), true) ?? Array.Empty<object>();
                SerializationCountAttribute? attribute = serializationCountAttributes.Length == 1 ? (SerializationCountAttribute)serializationCountAttributes[0] : null;

                if (attribute != null)
                {
                    Action<object, object?>? setProperty = propertyInfo.Set;
                    if (setProperty is not null)
                    {
                        propertyInfo.Set = (obj, value) =>
                        {
                            if (value != null)
                            {
                                // Increment the value by 1.
                                value = (int)value + 1;
                            }

                            setProperty (obj, value);
                        };
                    }
                }
            }
        }

        public static void RunIt()
        {
            var product = new Product
            {
                Name = "Aquafresh"
            };

            JsonSerializerOptions options = new()
            {
                TypeInfoResolver = new DefaultJsonTypeInfoResolver
                {
                    Modifiers = { IncrementCounterModifier }
                }
            };

            // First serialization and deserialization.
            string serialized = JsonSerializer.Serialize(product, options);
            Console.WriteLine(serialized);
            // {"Name":"Aquafresh","RoundTrips":0}

            Product deserialized = JsonSerializer.Deserialize<Product>(serialized, options)!;
            Console.WriteLine($"{deserialized.RoundTrips}");
            // 1

            // Second serialization and deserialization.
            serialized = JsonSerializer.Serialize(deserialized, options);
            Console.WriteLine(serialized);
            // { "Name":"Aquafresh","RoundTrips":1}

            deserialized = JsonSerializer.Deserialize<Product>(serialized, options)!;
            Console.WriteLine($"{deserialized.RoundTrips}");
            // 2
        }
    }
}

Beachten Sie in der Ausgabe, dass der Wert von RoundTrips jedes Mal erhöht wird, wenn die Product-Instanz deserialisiert wird.

Beispiel: Serialisieren privater Felder

Standardmäßig ignoriert System.Text.Json private Felder und Eigenschaften. In diesem Beispiel wird ein neues klassenweites Attribut (JsonIncludePrivateFieldsAttribute) hinzugefügt, um diesen Standard zu ändern. Wenn der Modifizierer das Attribut für einen Typ findet, fügt er alle privaten Felder für den Typ als neue Eigenschaften zu JsonTypeInfo hinzu.

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

namespace Serialization
{
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
    public class JsonIncludePrivateFieldsAttribute : Attribute { }

    [JsonIncludePrivateFields]
    public class Human
    {
        private string _name;
        private int _age;

        public Human()
        {
            // This constructor should be used only by deserializers.
            _name = null!;
            _age = 0;
        }

        public static Human Create(string name, int age)
        {
            Human h = new()
            {
                _name = name,
                _age = age
            };

            return h;
        }

        [JsonIgnore]
        public string Name
        {
            get => _name;
            set => throw new NotSupportedException();
        }

        [JsonIgnore]
        public int Age
        {
            get => _age;
            set => throw new NotSupportedException();
        }
    }

    public class PrivateFieldsExample
    {
        static void AddPrivateFieldsModifier(JsonTypeInfo jsonTypeInfo)
        {
            if (jsonTypeInfo.Kind != JsonTypeInfoKind.Object)
                return;

            if (!jsonTypeInfo.Type.IsDefined(typeof(JsonIncludePrivateFieldsAttribute), inherit: false))
                return;

            foreach (FieldInfo field in jsonTypeInfo.Type.GetFields(BindingFlags.Instance | BindingFlags.NonPublic))
            {
                JsonPropertyInfo jsonPropertyInfo = jsonTypeInfo.CreateJsonPropertyInfo(field.FieldType, field.Name);
                jsonPropertyInfo.Get = field.GetValue;
                jsonPropertyInfo.Set = field.SetValue;

                jsonTypeInfo.Properties.Add(jsonPropertyInfo);
            }
        }

        public static void RunIt()
        {
            var options = new JsonSerializerOptions
            {
                TypeInfoResolver = new DefaultJsonTypeInfoResolver
                {
                    Modifiers = { AddPrivateFieldsModifier }
                }
            };

            var human = Human.Create("Julius", 37);
            string json = JsonSerializer.Serialize(human, options);
            Console.WriteLine(json);
            // {"_name":"Julius","_age":37}

            Human deserializedHuman = JsonSerializer.Deserialize<Human>(json, options)!;
            Console.WriteLine($"[Name={deserializedHuman.Name}; Age={deserializedHuman.Age}]");
            // [Name=Julius; Age=37]
        }
    }
}

Tipp

Wenn die Namen Ihrer privaten Felder mit Unterstrichen beginnen, sollten Sie die Unterstriche aus den Namen entfernen, wenn Sie die Felder als neue JSON-Eigenschaften hinzufügen.

Beispiel: Ignorieren von Eigenschaften eines bestimmten Typs

Möglicherweise verfügt Ihr Modell über Eigenschaften mit bestimmten Namen oder Typen, die Benutzer nicht erfahren sollen. Beispielsweise könnten Sie über eine Eigenschaft verfügen, die Anmeldeinformationen speichert oder Informationen, die in der Nutzlast nichts zu suchen haben.

Das folgende Beispiel zeigt, wie Sie Eigenschaften mit einem bestimmten Typ herausfiltern, SecretHolder. Dazu wird eine IList<T>-Erweiterungsmethode verwendet, um alle Eigenschaften mit dem angegebenen Typ aus der JsonTypeInfo.Properties-Liste zu entfernen. Die gefilterten Eigenschaften verschwinden vollständig aus dem Vertrag, d. h. System.Text.Json berücksichtigt sie weder während der Serialisierung noch während der Deserialisierung.

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

namespace Serialization
{
    class ExampleClass
    {
        public string Name { get; set; } = "";
        public SecretHolder? Secret { get; set; }
    }

    class SecretHolder
    {
        public string Value { get; set; } = "";
    }

    class IgnorePropertiesWithType
    {
        private readonly Type[] _ignoredTypes;

        public IgnorePropertiesWithType(params Type[] ignoredTypes)
            => _ignoredTypes = ignoredTypes;

        public void ModifyTypeInfo(JsonTypeInfo ti)
        {
            if (ti.Kind != JsonTypeInfoKind.Object)
                return;

            ti.Properties.RemoveAll(prop => _ignoredTypes.Contains(prop.PropertyType));
        }
    }

    public class IgnoreTypeExample
    {
        public static void RunIt()
        {
            var modifier = new IgnorePropertiesWithType(typeof(SecretHolder));

            JsonSerializerOptions options = new()
            {
                TypeInfoResolver = new DefaultJsonTypeInfoResolver
                {
                    Modifiers = { modifier.ModifyTypeInfo }
                }
            };

            ExampleClass obj = new()
            {
                Name = "Password",
                Secret = new SecretHolder { Value = "MySecret" }
            };

            string output = JsonSerializer.Serialize(obj, options);
            Console.WriteLine(output);
            // {"Name":"Password"}
        }
    }

    public static class ListHelpers
    {
        // IList<T> implementation of List<T>.RemoveAll method.
        public static void RemoveAll<T>(this IList<T> list, Predicate<T> predicate)
        {
            for (int i = 0; i < list.Count; i++)
            {
                if (predicate(list[i]))
                {
                    list.RemoveAt(i--);
                }
            }
        }
    }
}

Beispiel: Zulassen, dass int-Werte Zeichenfolgen sind

Möglicherweise kann in Ihrem Eingabe-JSON-Code einer der numerischen Typen von Anführungszeichen umschlossen sein, andere jedoch nicht. Wenn Sie die Kontrolle über die Klasse hätten, könnten Sie JsonNumberHandlingAttribute für den Typ festlegen, um dies zu beheben, aber dies ist nicht der Fall. Vor .NET 7 mussten Sie einen benutzerdefinierten Konverter schreiben, um dieses Verhalten zu beheben, wozu Sie einiges an Code schreiben mussten. Mithilfe der Vertragsanpassung können Sie das Verhalten der Zahlenbehandlung für jeden Typ anpassen.

Im folgenden Beispiel wird das Verhalten für alle int-Werte geändert. Das Beispiel kann problemlos angepasst werden, um es auf einen beliebigen Typ oder auf eine bestimmte Eigenschaft eines beliebigen Typs anzuwenden.

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

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

    public class AllowIntsAsStringsExample
    {
        static void SetNumberHandlingModifier(JsonTypeInfo jsonTypeInfo)
        {
            if (jsonTypeInfo.Type == typeof(int))
            {
                jsonTypeInfo.NumberHandling = JsonNumberHandling.AllowReadingFromString;
            }
        }

        public static void RunIt()
        {
            JsonSerializerOptions options = new()
            {
                TypeInfoResolver = new DefaultJsonTypeInfoResolver
                {
                    Modifiers = { SetNumberHandlingModifier }
                }
            };

            // Triple-quote syntax is a C# 11 feature.
            Point point = JsonSerializer.Deserialize<Point>("""{"X":"12","Y":"3"}""", options)!;
            Console.WriteLine($"({point.X},{point.Y})");
            // (12,3)
        }
    }
}

Wäre dem Modifizierer nicht erlaubt gewesen, int-Werte aus einer Zeichenfolge zu lesen, wäre das Programm mit einem Ausnahmefehler beendet worden:

Ausnahmefehler. System.Text.Json.JsonException: Der JSON-Wert konnte nicht in System.Int32 konvertiert werden. Pfad: $.X | LineNumber: 0 | BytePositionInLine: 9.

Weitere Möglichkeiten zum Anpassen der Serialisierung

Neben dem Anpassen eines Vertrags gibt es weitere Möglichkeiten, das Serialisierungs- und Deserialisierungsverhalten zu beeinflussen, einschließlich der folgenden:

  • Mithilfe von Attributen, die von JsonAttribute abgeleitet werden, z. B. JsonIgnoreAttribute und JsonPropertyOrderAttribute.
  • Durch Ändern von JsonSerializerOptions, um z. B. eine Benennungsrichtlinie festzulegen oder Enumerationswerte als Zeichenfolgen anstelle von Zahlen zu serialisieren.
  • Durch Schreiben eines benutzerdefinierten Konverters, der die eigentliche Arbeit des Schreibens des JSON-Codes und, während der Deserialisierung, des Erstellens eines Objekts übernimmt.

Die Anpassung von Verträgen ist eine Verbesserung gegenüber diesen bereits existierenden Anpassungen, da Sie möglicherweise keinen Zugriff auf den Typ haben, um Attribute hinzuzufügen. Darüber hinaus ist das Schreiben eines angepassten Konverters komplex und beeinträchtigt die Leistung.

Siehe auch