Anpassa ett JSON-kontrakt
Biblioteket System.Text.Json konstruerar ett JSON-kontrakt för varje .NET-typ, som definierar hur typen ska serialiseras och deserialiseras. Kontraktet härleds från typens form, som innehåller egenskaper som egenskaper och fält och om det implementerar IEnumerable gränssnittet eller IDictionary . Typer mappas till kontrakt antingen vid körning med reflektion eller vid kompilering med hjälp av källgeneratorn.
Från och med .NET 7 kan du anpassa dessa JSON-kontrakt för att ge mer kontroll över hur typer konverteras till JSON och vice versa. I följande lista visas bara några exempel på vilka typer av anpassningar du kan göra för serialisering och deserialisering:
- Serialisera privata fält och egenskaper.
- Stöd för flera namn för en enskild egenskap (till exempel om en tidigare biblioteksversion använde ett annat namn).
- Ignorera egenskaper med ett specifikt namn, typ eller värde.
- Skilja mellan explicita
null
värden och bristen på ett värde i JSON-nyttolasten. - Stödattribut System.Runtime.Serialization , till exempel DataContractAttribute. Mer information finns i Attribut för System.Runtime.Serialization.
- Utlöser ett undantag om JSON innehåller en egenskap som inte är en del av måltypen. Mer information finns i Hantera medlemmar som saknas.
Så här anmäler du dig
Det finns två sätt att ansluta till anpassning. Båda handlar om att hämta en lösning, vars jobb är att tillhandahålla en JsonTypeInfo instans för varje typ som måste serialiseras.
Genom att anropa DefaultJsonTypeInfoResolver() konstruktorn för att hämta JsonSerializerOptions.TypeInfoResolver och lägga till dina anpassade åtgärder i dess Modifiers egenskap.
Till exempel:
JsonSerializerOptions options = new() { TypeInfoResolver = new DefaultJsonTypeInfoResolver { Modifiers = { MyCustomModifier1, MyCustomModifier2 } } };
Om du lägger till flera modifierare anropas de sekventiellt.
Genom att skriva en anpassad lösning som implementerar IJsonTypeInfoResolver.
- Om en typ inte hanteras IJsonTypeInfoResolver.GetTypeInfo bör den returneras
null
för den typen. - Du kan också kombinera din anpassade matchare med andra, till exempel standardlösaren. Matcharna kommer att frågas i ordning tills ett värde som inte är null JsonTypeInfo returneras för typen.
- Om en typ inte hanteras IJsonTypeInfoResolver.GetTypeInfo bör den returneras
Konfigurerbara aspekter
Egenskapen JsonTypeInfo.Kind anger hur konverteraren serialiserar en viss typ, till exempel som ett objekt eller som en matris, och om dess egenskaper serialiseras. Du kan fråga den här egenskapen för att avgöra vilka aspekter av en typs JSON-kontrakt som du kan konfigurera. Det finns fyra olika typer:
JsonTypeInfo.Kind |
beskrivning |
---|---|
JsonTypeInfoKind.Object | Konverteraren serialiserar typen till ett JSON-objekt och använder dess egenskaper. Den här typen används för de flesta klass- och structtyper och ger mest flexibilitet. |
JsonTypeInfoKind.Enumerable | Konverteraren serialiserar typen till en JSON-matris. Den här typen används för typer som List<T> och matriser. |
JsonTypeInfoKind.Dictionary | Konverteraren serialiserar typen till ett JSON-objekt. Den här typen används för typer som Dictionary<K, V> . |
JsonTypeInfoKind.None | Konverteraren anger inte hur den ska serialisera typen eller vilka JsonTypeInfo egenskaper den ska använda. Den här typen används för typer som System.Object, int och string och för alla typer som använder en anpassad konverterare. |
Modifierare
En modifierare är en Action<JsonTypeInfo>
eller en metod med en JsonTypeInfo parameter som hämtar kontraktets aktuella tillstånd som argument och gör ändringar i kontraktet. Du kan till exempel iterera genom de förifyllda egenskaperna på den angivna JsonTypeInfo för att hitta den du är intresserad av och sedan ändra dess JsonPropertyInfo.Get egenskap (för serialisering) eller JsonPropertyInfo.Set egenskap (för deserialisering). Eller så kan du skapa en ny egenskap med och JsonTypeInfo.CreateJsonPropertyInfo(Type, String) lägga till den i JsonTypeInfo.Properties samlingen.
I följande tabell visas de ändringar du kan göra och hur du uppnår dem.
Ändring | Tillämplig JsonTypeInfo.Kind |
Så här gör du för att uppnå det | Exempel |
---|---|---|---|
Anpassa en egenskaps värde | JsonTypeInfoKind.Object |
Ändra ombudet JsonPropertyInfo.Get (för serialisering) eller JsonPropertyInfo.Set ombudet (för deserialisering) för egenskapen. | Öka värdet för en egenskap |
Lägga till eller ta bort egenskaper | JsonTypeInfoKind.Object |
Lägg till eller ta bort objekt från JsonTypeInfo.Properties listan. | Serialisera privata fält |
Serialisera en egenskap villkorligt | JsonTypeInfoKind.Object |
Ändra predikatet JsonPropertyInfo.ShouldSerialize för egenskapen. | Ignorera egenskaper med en viss typ |
Anpassa nummerhantering för en viss typ | JsonTypeInfoKind.None |
JsonTypeInfo.NumberHandling Ändra värdet för typen. | Tillåt att int-värden är strängar |
Exempel: Öka värdet för en egenskap
Tänk dig följande exempel där modifieraren ökar värdet för en viss egenskap vid deserialisering genom att ändra dess JsonPropertyInfo.Set ombud. Förutom att definiera modifieraren introducerar exemplet även ett nytt attribut som används för att hitta egenskapen vars värde ska ökas. Det här är ett exempel på hur du anpassar en egenskap.
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
}
}
}
Observera i utdata att värdet RoundTrips
för ökas varje gång instansen Product
deserialiseras.
Exempel: Serialisera privata fält
Som standard System.Text.Json
ignorerar privata fält och egenskaper. Det här exemplet lägger till ett nytt klassomfattande attribut, JsonIncludePrivateFieldsAttribute
, för att ändra standardvärdet. Om modifieraren hittar attributet för en typ lägger den till alla privata fält på typen som nya egenskaper till 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]
}
}
}
Dricks
Om dina privata fältnamn börjar med understreck bör du överväga att ta bort understrecken från namnen när du lägger till fälten som nya JSON-egenskaper.
Exempel: Ignorera egenskaper med en viss typ
Din modell kanske har egenskaper med specifika namn eller typer som du inte vill exponera för användare. Du kan till exempel ha en egenskap som lagrar autentiseringsuppgifter eller viss information som är värdelös att ha i nyttolasten.
I följande exempel visas hur du filtrerar bort egenskaper med en viss typ, SecretHolder
. Det gör det med hjälp av en IList<T> tilläggsmetod för att ta bort alla egenskaper som har den angivna typen från JsonTypeInfo.Properties listan. De filtrerade egenskaperna försvinner helt från kontraktet, vilket innebär att System.Text.Json
de inte tittar på dem under serialisering eller deserialisering.
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--);
}
}
}
}
}
Exempel: Tillåt att int-värden är strängar
Kanske kan din indata-JSON innehålla citattecken runt någon av de numeriska typerna, men inte på andra. Om du hade kontroll över klassen kan du placera JsonNumberHandlingAttribute på typen för att åtgärda detta, men det gör du inte. Innan .NET 7 skulle du behöva skriva en anpassad konverterare för att åtgärda det här beteendet, vilket kräver att du skriver en hel del kod. Med hjälp av kontraktsanpassning kan du anpassa beteendet för nummerhantering för valfri typ.
I följande exempel ändras beteendet för alla int
värden. Exemplet kan enkelt justeras för att gälla för valfri typ eller för en specifik egenskap av vilken typ som helst.
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)
}
}
}
Utan modifieraren för att tillåta läsningsvärden int
från en sträng skulle programmet ha slutat med ett undantag:
Ohanterat undantag. System.Text.Json.JsonException: JSON-värdet kunde inte konverteras till System.Int32. Sökväg: $. X | LineNumber: 0 | BytePositionInLine: 9.
Andra sätt att anpassa serialisering
Förutom att anpassa ett kontrakt finns det andra sätt att påverka serialiserings- och deserialiseringsbeteende, inklusive följande:
- Genom att använda attribut som härletts från JsonAttribute, till exempel JsonIgnoreAttribute och JsonPropertyOrderAttribute.
- Genom att JsonSerializerOptionsändra , till exempel för att ange en namngivningsprincip eller serialisera uppräkningsvärden som strängar i stället för tal.
- Genom att skriva en anpassad konverterare som utför det faktiska arbetet med att skriva JSON och, under deserialisering, konstruera ett objekt.
Kontraktsanpassning är en förbättring jämfört med dessa befintliga anpassningar eftersom du kanske inte har åtkomst till typen för att lägga till attribut. Att skriva en anpassad konverterare är dessutom komplext och skadar prestandan.