Dostosowywanie kontraktu JSON
Biblioteka System.Text.Json tworzy kontrakt JSON dla każdego typu platformy .NET, który definiuje sposób serializacji i deserializacji typu. Kontrakt pochodzi z kształtu typu, który zawiera cechy, takie jak jego właściwości i pola oraz czy implementuje IEnumerable interfejs lub IDictionary . Typy są mapowane na kontrakty w czasie wykonywania przy użyciu odbicia lub w czasie kompilacji przy użyciu generatora źródła.
Począwszy od platformy .NET 7, możesz dostosować te kontrakty JSON, aby zapewnić większą kontrolę nad sposobem konwertowania typów na format JSON i odwrotnie. Na poniższej liście przedstawiono tylko kilka przykładów typów dostosowań, które można wprowadzić w celu serializacji i deserializacji:
- Serializowanie prywatnych pól i właściwości.
- Obsługa wielu nazw pojedynczej właściwości (na przykład jeśli poprzednia wersja biblioteki użyła innej nazwy).
- Ignoruj właściwości o określonej nazwie, typie lub wartości.
- Rozróżnianie jawnych
null
wartości i brak wartości w ładunku JSON. - Atrybuty obsługi System.Runtime.Serialization , takie jak DataContractAttribute. Aby uzyskać więcej informacji, zobacz Atrybuty System.Runtime.Serialization.
- Zgłaszanie wyjątku, jeśli kod JSON zawiera właściwość, która nie jest częścią typu docelowego. Aby uzyskać więcej informacji, zobacz Obsługa brakujących elementów członkowskich.
Jak wyrazić zgodę
Istnieją dwa sposoby podłączania do dostosowywania. Oba obejmują uzyskanie narzędzia rozpoznawania nazw, którego zadaniem jest udostępnienie JsonTypeInfo wystąpienia dla każdego typu, który musi być serializowany.
Wywołując konstruktor w DefaultJsonTypeInfoResolver() celu uzyskania i dodania JsonSerializerOptions.TypeInfoResolver akcji niestandardowych do jej Modifiers właściwości.
Na przykład:
JsonSerializerOptions options = new() { TypeInfoResolver = new DefaultJsonTypeInfoResolver { Modifiers = { MyCustomModifier1, MyCustomModifier2 } } };
Jeśli dodasz wiele modyfikatorów, będą one wywoływane sekwencyjnie.
Pisząc niestandardowy program rozpoznawania nazw, który implementuje IJsonTypeInfoResolverelement .
- Jeśli typ nie jest obsługiwany, IJsonTypeInfoResolver.GetTypeInfo powinien zostać zwrócony
null
dla tego typu. - Możesz również połączyć niestandardowy program rozpoznawania nazw z innymi, na przykład domyślnym modułem rozpoznawania. Narzędzia rozpoznawania nazw będą odpytywane w kolejności do momentu zwrócenia wartości innej niż null JsonTypeInfo dla typu.
- Jeśli typ nie jest obsługiwany, IJsonTypeInfoResolver.GetTypeInfo powinien zostać zwrócony
Konfigurowalne aspekty
Właściwość JsonTypeInfo.Kind wskazuje, jak konwerter serializuje dany typ — na przykład jako obiekt lub jako tablicę oraz czy jego właściwości są serializowane. Możesz wykonać zapytanie dotyczące tej właściwości, aby określić, które aspekty kontraktu JSON typu można skonfigurować. Istnieją cztery różne rodzaje:
JsonTypeInfo.Kind |
opis |
---|---|
JsonTypeInfoKind.Object | Konwerter serializuje typ do obiektu JSON i używa jego właściwości. Ten rodzaj jest używany dla większości typów klas i struktur i zapewnia największą elastyczność. |
JsonTypeInfoKind.Enumerable | Konwerter serializuje typ do tablicy JSON. Ten rodzaj jest używany dla typów, takich jak List<T> i tablica. |
JsonTypeInfoKind.Dictionary | Konwerter serializuje typ do obiektu JSON. Ten rodzaj jest używany dla typów, takich jak Dictionary<K, V> . |
JsonTypeInfoKind.None | Konwerter nie określa, jak będzie serializować typ lub jakie JsonTypeInfo właściwości będą używane. Ten rodzaj jest używany dla typów, takich jak System.Object, int i , i string dla wszystkich typów, które używają konwertera niestandardowego. |
Modyfikatory
Modyfikator to Action<JsonTypeInfo>
metoda lub z parametrem JsonTypeInfo , który pobiera bieżący stan kontraktu jako argument i wprowadza modyfikacje kontraktu. Można na przykład wykonać iterację po wstępnie wypełnionych właściwościach określonych JsonTypeInfo , aby znaleźć odpowiednią właściwość, a następnie zmodyfikować jej JsonPropertyInfo.Get właściwość (na potrzeby serializacji) lub JsonPropertyInfo.Set właściwość (w celu deserializacji). Możesz też utworzyć nową właściwość przy użyciu metody JsonTypeInfo.CreateJsonPropertyInfo(Type, String) i dodać ją do kolekcji JsonTypeInfo.Properties .
W poniższej tabeli przedstawiono modyfikacje, które można wprowadzić i jak je osiągnąć.
Modyfikacji | Odpowiedni JsonTypeInfo.Kind |
Jak to osiągnąć | Przykład |
---|---|---|---|
Dostosowywanie wartości właściwości | JsonTypeInfoKind.Object |
Zmodyfikuj JsonPropertyInfo.Get delegata (na potrzeby serializacji) lub JsonPropertyInfo.Set delegata (na potrzeby deserializacji) dla właściwości . | Zwiększanie wartości właściwości |
Dodawanie lub usuwanie właściwości | JsonTypeInfoKind.Object |
Dodaj lub usuń elementy z JsonTypeInfo.Properties listy. | Serializowanie pól prywatnych |
Warunkowe serializowanie właściwości | JsonTypeInfoKind.Object |
Zmodyfikuj JsonPropertyInfo.ShouldSerialize predykat dla właściwości . | Ignoruj właściwości o określonym typie |
Dostosowywanie obsługi numerów dla określonego typu | JsonTypeInfoKind.None |
Zmodyfikuj JsonTypeInfo.NumberHandling wartość typu. | Zezwalaj na wartości int jako ciągi |
Przykład: zwiększanie wartości właściwości
Rozważmy następujący przykład, w którym modyfikator zwiększa wartość określonej właściwości w deserializacji, modyfikując jego JsonPropertyInfo.Set delegata. Oprócz definiowania modyfikatora przykład wprowadza również nowy atrybut używany do lokalizowania właściwości, której wartość powinna być zwiększana. Jest to przykład dostosowywania właściwości.
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
}
}
}
Zwróć uwagę, że w danych wyjściowych RoundTrips
wartość jest zwiększana za każdym razem, Product
gdy wystąpienie jest deserializowane.
Przykład: Serializowanie pól prywatnych
Domyślnie System.Text.Json
ignoruje pola prywatne i właściwości. W tym przykładzie dodano nowy atrybut dla całej klasy , JsonIncludePrivateFieldsAttribute
aby zmienić to ustawienie domyślne. Jeśli modyfikator znajdzie atrybut typu, dodaje wszystkie pola prywatne w typie jako nowe właściwości do JsonTypeInfo.
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]
}
}
}
Napiwek
Jeśli nazwy pól prywatnych zaczynają się od podkreśleń, rozważ usunięcie podkreśleń z nazw podczas dodawania pól jako nowych właściwości JSON.
Przykład: Ignoruj właściwości o określonym typie
Być może model ma właściwości o określonych nazwach lub typach, których nie chcesz ujawniać użytkownikom. Na przykład może istnieć właściwość, która przechowuje poświadczenia lub niektóre informacje, które są bezużyteczne do stosowania w ładunku.
W poniższym przykładzie pokazano, jak odfiltrować właściwości przy użyciu określonego typu : SecretHolder
. Robi to przy użyciu IList<T> metody rozszerzenia, aby usunąć wszystkie właściwości, które mają określony typ z JsonTypeInfo.Properties listy. Odfiltrowane właściwości całkowicie znikają z kontraktu, co oznacza System.Text.Json
, że nie są one uwzględniane podczas serializacji lub deserializacji.
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--);
}
}
}
}
}
Przykład: zezwalanie na wartości int jako ciągi
Być może wejściowy kod JSON może zawierać cudzysłowy wokół jednego z typów liczbowych, ale nie innych. Jeśli masz kontrolę nad klasą, możesz umieścić JsonNumberHandlingAttribute typ, aby rozwiązać ten problem, ale nie. Przed platformą .NET 7 należy napisać konwerter niestandardowy, aby naprawić to zachowanie, co wymaga pisania sporo kodu. Za pomocą dostosowywania kontraktu można dostosować zachowanie obsługi liczb dla dowolnego typu.
Poniższy przykład zmienia zachowanie wszystkich int
wartości. Przykład można łatwo dostosować do dowolnego typu lub dla określonej właściwości dowolnego typu.
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)
}
}
}
Bez modyfikatora zezwalającego na odczytywanie int
wartości z ciągu program zakończyłby się wyjątkiem:
Nieobsługiwany wyjątek. System.Text.Json.JsonException: nie można przekonwertować wartości JSON na System.Int32. Ścieżka: $. X | Numer wiersza: 0 | BytePositionInLine: 9.
Inne sposoby dostosowywania serializacji
Oprócz dostosowywania kontraktu istnieją inne sposoby wpływu na zachowanie serializacji i deserializacji, w tym następujące:
- Przy użyciu atrybutów pochodzących z JsonAttribute, na przykład JsonIgnoreAttribute i JsonPropertyOrderAttribute.
- Modyfikując JsonSerializerOptionsna przykład , aby ustawić zasady nazewnictwa lub serializować wartości wyliczenia jako ciągi zamiast liczb.
- Pisząc konwerter niestandardowy, który wykonuje rzeczywistą pracę zapisu w formacie JSON i podczas deserializacji, konstruowania obiektu.
Dostosowanie kontraktu jest ulepszeniem tych wstępnie istniejących dostosowań, ponieważ może nie mieć dostępu do typu w celu dodania atrybutów. Ponadto pisanie konwertera niestandardowego jest złożone i boli wydajność.