Jak serializować właściwości klas pochodnych za pomocą polecenia System.Text.Json
Z tego artykułu dowiesz się, jak serializować właściwości klas pochodnych za pomocą System.Text.Json
przestrzeni nazw.
Serializowanie właściwości klas pochodnych
Począwszy od platformy .NET 7, System.Text.Json
obsługuje serializacji hierarchii typów polimorficznych i deserializacji z adnotacjami atrybutów.
Atrybut | opis |
---|---|
JsonDerivedTypeAttribute | W przypadku umieszczenia na deklaracji typu wskazuje, że określony podtyp powinien zostać wybrany do serializacji polimorficznej. Uwidacznia również możliwość określenia dyskryminującego typu. |
JsonPolymorphicAttribute | W przypadku umieszczenia na deklaracji typu wskazuje, że typ powinien być serializowany polimorficznie. Udostępnia również różne opcje konfigurowania serializacji polimorficznej i deserializacji dla tego typu. |
Załóżmy na przykład, że masz klasę i klasę WeatherForecastBase
WeatherForecastWithCity
pochodną :
[JsonDerivedType(typeof(WeatherForecastWithCity))]
public class WeatherForecastBase
{
public DateTimeOffset Date { get; set; }
public int TemperatureCelsius { get; set; }
public string? Summary { get; set; }
}
<JsonDerivedType(GetType(WeatherForecastWithCity))>
Public Class WeatherForecastBase
Public Property [Date] As DateTimeOffset
Public Property TemperatureCelsius As Integer
Public Property Summary As String
End Class
public class WeatherForecastWithCity : WeatherForecastBase
{
public string? City { get; set; }
}
Public Class WeatherForecastWithCity
Inherits WeatherForecastBase
Public Property City As String
End Class
Załóżmy, że argumentem Serialize<TValue>
typu metody w czasie kompilacji jest WeatherForecastBase
:
options = new JsonSerializerOptions
{
WriteIndented = true
};
jsonString = JsonSerializer.Serialize<WeatherForecastBase>(weatherForecastBase, options);
options = New JsonSerializerOptions With {
.WriteIndented = True
}
jsonString = JsonSerializer.Serialize(WeatherForecastBase, options)
W tym scenariuszu City
właściwość jest serializowana, ponieważ weatherForecastBase
obiekt jest w rzeczywistości obiektem WeatherForecastWithCity
. Ta konfiguracja umożliwia serializacji polimorficznej dla WeatherForecastBase
programu , w szczególności w przypadku, gdy typ środowiska uruchomieniowego to WeatherForecastWithCity
:
{
"City": "Milwaukee",
"Date": "2022-09-26T00:00:00-05:00",
"TemperatureCelsius": 15,
"Summary": "Cool"
}
Podczas gdy zaokrąglanie ładunku w taki sposób, jak WeatherForecastBase
jest obsługiwane, nie zmaterializuje się jako typ WeatherForecastWithCity
czasu wykonywania . Zamiast tego zmaterializuje się jako typ WeatherForecastBase
czasu wykonywania :
WeatherForecastBase value = JsonSerializer.Deserialize<WeatherForecastBase>("""
{
"City": "Milwaukee",
"Date": "2022-09-26T00:00:00-05:00",
"TemperatureCelsius": 15,
"Summary": "Cool"
}
""");
Console.WriteLine(value is WeatherForecastWithCity); // False
Dim value As WeatherForecastBase = JsonSerializer.Deserialize(@"
{
"City": "Milwaukee",
"Date": "2022-09-26T00:00:00-05:00",
"TemperatureCelsius": 15,
"Summary": "Cool"
}")
Console.WriteLine(value is WeatherForecastWithCity) // False
W poniższej sekcji opisano sposób dodawania metadanych w celu włączenia zaokrąglania typu pochodnego.
Dyskryminacje typu polimorficznego
Aby włączyć deserializację polimorficzną, należy określić dyskryminację typu dla klasy pochodnej:
[JsonDerivedType(typeof(WeatherForecastBase), typeDiscriminator: "base")]
[JsonDerivedType(typeof(WeatherForecastWithCity), typeDiscriminator: "withCity")]
public class WeatherForecastBase
{
public DateTimeOffset Date { get; set; }
public int TemperatureCelsius { get; set; }
public string? Summary { get; set; }
}
public class WeatherForecastWithCity : WeatherForecastBase
{
public string? City { get; set; }
}
<JsonDerivedType(GetType(WeatherForecastBase), "base")>
<JsonDerivedType(GetType(WeatherForecastWithCity), "withCity")>
Public Class WeatherForecastBase
Public Property [Date] As DateTimeOffset
Public Property TemperatureCelsius As Integer
Public Property Summary As String
End Class
Public Class WeatherForecastWithCity
Inherits WeatherForecastBase
Public Property City As String
End Class
W przypadku dodanych metadanych, w szczególności dyskryminujących typ, serializator może serializować i deserializować ładunek jako typ z jego typu WeatherForecastBase
podstawowego WeatherForecastWithCity
. Serializacja emituje kod JSON wraz z metadanymi dyskryminującymi typu:
WeatherForecastBase weather = new WeatherForecastWithCity
{
City = "Milwaukee",
Date = new DateTimeOffset(2022, 9, 26, 0, 0, 0, TimeSpan.FromHours(-5)),
TemperatureCelsius = 15,
Summary = "Cool"
}
var json = JsonSerializer.Serialize<WeatherForecastBase>(weather, options);
Console.WriteLine(json);
// Sample output:
// {
// "$type" : "withCity",
// "City": "Milwaukee",
// "Date": "2022-09-26T00:00:00-05:00",
// "TemperatureCelsius": 15,
// "Summary": "Cool"
// }
Dim weather As WeatherForecastBase = New WeatherForecastWithCity With
{
.City = "Milwaukee",
.[Date] = New DateTimeOffset(2022, 9, 26, 0, 0, 0, TimeSpan.FromHours(-5)),
.TemperatureCelsius = 15,
.Summary = "Cool"
}
Dim json As String = JsonSerializer.Serialize(weather, options)
Console.WriteLine(json)
' Sample output:
' {
' "$type" : "withCity",
' "City": "Milwaukee",
' "Date": "2022-09-26T00:00:00-05:00",
' "TemperatureCelsius": 15,
' "Summary": "Cool"
' }
Z dyskryminującym typem serializator może deserializować ładunek polimorficznie jako WeatherForecastWithCity
:
WeatherForecastBase value = JsonSerializer.Deserialize<WeatherForecastBase>(json);
Console.WriteLine(value is WeatherForecastWithCity); // True
Dim value As WeatherForecastBase = JsonSerializer.Deserialize(json)
Console.WriteLine(value is WeatherForecastWithCity) // True
Uwaga
Domyślnie $type
dyskryminator musi zostać umieszczony na początku obiektu JSON, pogrupowany razem z innymi właściwościami metadanych, takimi jak $id
i $ref
. Jeśli odczytujesz dane z zewnętrznego interfejsu API, który umieszcza $type
dyskryminujące w środku obiektu JSON, ustaw wartość JsonSerializerOptions.AllowOutOfOrderMetadataProperties na true
:
JsonSerializerOptions options = new() { AllowOutOfOrderMetadataProperties = true };
JsonSerializer.Deserialize<Base>("""{"Name":"Name","$type":"derived"}""", options);
Podczas włączania tej flagi należy zachować ostrożność, ponieważ może to spowodować nadmierne buforowanie (i awarie poza pamięcią) podczas wykonywania deserializacji przesyłania strumieniowego bardzo dużych obiektów JSON.
Formaty dyskryminujące kombinacji i dopasowywania typów
Identyfikatory dyskryminujące typu są prawidłowe w formularzach string
lub int
, więc następujące elementy są prawidłowe:
[JsonDerivedType(typeof(WeatherForecastWithCity), 0)]
[JsonDerivedType(typeof(WeatherForecastWithTimeSeries), 1)]
[JsonDerivedType(typeof(WeatherForecastWithLocalNews), 2)]
public class WeatherForecastBase { }
var json = JsonSerializer.Serialize<WeatherForecastBase>(new WeatherForecastWithTimeSeries());
Console.WriteLine(json);
// Sample output:
// {
// "$type" : 1,
// Omitted for brevity...
// }
<JsonDerivedType(GetType(WeatherForecastWithCity), 0)>
<JsonDerivedType(GetType(WeatherForecastWithTimeSeries), 1)>
<JsonDerivedType(GetType(WeatherForecastWithLocalNews), 2)>
Public Class WeatherForecastBase
End Class
Dim json As String = JsonSerializer.Serialize(Of WeatherForecastBase)(New WeatherForecastWithTimeSeries())
Console.WriteLine(json)
' Sample output:
' {
' "$type" : 1,
' Omitted for brevity...
' }
Interfejs API obsługuje mieszanie i dopasowywanie konfiguracji dyskryminujących typów, ale nie jest to zalecane. Ogólne zalecenie polega na wykorzystaniu wszystkich string
typów dyskryminujących, dyskryminujących wszystkich int
typów lub brak dyskryminujących w ogóle. W poniższym przykładzie pokazano, jak mieszać i dopasowywać konfiguracje dyskryminujące typu:
[JsonDerivedType(typeof(ThreeDimensionalPoint), typeDiscriminator: 3)]
[JsonDerivedType(typeof(FourDimensionalPoint), typeDiscriminator: "4d")]
public class BasePoint
{
public int X { get; set; }
public int Y { get; set; }
}
public class ThreeDimensionalPoint : BasePoint
{
public int Z { get; set; }
}
public sealed class FourDimensionalPoint : ThreeDimensionalPoint
{
public int W { get; set; }
}
<JsonDerivedType(GetType(ThreeDimensionalPoint), 3)>
<JsonDerivedType(GetType(FourDimensionalPoint), "4d")>
Public Class BasePoint
Public Property X As Integer
Public Property Y As Integer
End Class
Public Class ThreeDimensionalPoint
Inherits BasePoint
Public Property Z As Integer
End Class
Public NotInheritable Class FourDimensionalPoint
Inherits ThreeDimensionalPoint
Public Property W As Integer
End Class
W poprzednim przykładzie BasePoint
typ nie ma dyskryminującego typu, podczas gdy ThreeDimensionalPoint
typ ma int
dyskryminujący typ, a FourDimensionalPoint
typ ma string
dyskryminujący typ.
Ważne
Aby serializacja polimorficzna działała, typ wartości serializowanej powinien być typu polimorficznego typu podstawowego. Obejmuje to użycie typu podstawowego jako parametru typu ogólnego podczas serializacji wartości na poziomie głównym, jako zadeklarowanego typu właściwości serializacji lub jako elementu kolekcji w serializacji kolekcji.
using System.Text.Json;
using System.Text.Json.Serialization;
PerformRoundTrip<BasePoint>();
PerformRoundTrip<ThreeDimensionalPoint>();
PerformRoundTrip<FourDimensionalPoint>();
static void PerformRoundTrip<T>() where T : BasePoint, new()
{
var json = JsonSerializer.Serialize<BasePoint>(new T());
Console.WriteLine(json);
BasePoint? result = JsonSerializer.Deserialize<BasePoint>(json);
Console.WriteLine($"result is {typeof(T)}; // {result is T}");
Console.WriteLine();
}
// Sample output:
// { "X": 541, "Y": 503 }
// result is BasePoint; // True
//
// { "$type": 3, "Z": 399, "X": 835, "Y": 78 }
// result is ThreeDimensionalPoint; // True
//
// { "$type": "4d", "W": 993, "Z": 427, "X": 508, "Y": 741 }
// result is FourDimensionalPoint; // True
Imports System.Text.Json
Imports System.Text.Json.Serialization
Module Program
Sub Main()
PerformRoundTrip(Of BasePoint)()
PerformRoundTrip(Of ThreeDimensionalPoint)()
PerformRoundTrip(Of FourDimensionalPoint)()
End Sub
Private Sub PerformRoundTrip(Of T As {BasePoint, New})()
Dim json = JsonSerializer.Serialize(Of BasePoint)(New T())
Console.WriteLine(json)
Dim result As BasePoint = JsonSerializer.Deserialize(Of BasePoint)(json)
Console.WriteLine($"result is {GetType(T)}; // {TypeOf result Is T}")
Console.WriteLine()
End Sub
End Module
' Sample output:
' { "X": 649, "Y": 754 }
' result is BasePoint; // True
'
' { "$type": 3, "Z": 247, "X": 814, "Y": 56 }
' result is ThreeDimensionalPoint; // True
'
' { "$type": "4d", "W": 427, "Z": 193, "X": 112, "Y": 935 }
' result is FourDimensionalPoint; // True
Dostosowywanie nazwy dyskryminującej typu
Domyślna nazwa właściwości dla typu dyskryminującego to $type
. Aby dostosować nazwę właściwości, użyj elementu JsonPolymorphicAttribute , jak pokazano w poniższym przykładzie:
[JsonPolymorphic(TypeDiscriminatorPropertyName = "$discriminator")]
[JsonDerivedType(typeof(ThreeDimensionalPoint), typeDiscriminator: "3d")]
public class BasePoint
{
public int X { get; set; }
public int Y { get; set; }
}
public sealed class ThreeDimensionalPoint : BasePoint
{
public int Z { get; set; }
}
<JsonPolymorphic(TypeDiscriminatorPropertyName:="$discriminator")>
<JsonDerivedType(GetType(ThreeDimensionalPoint), "3d")>
Public Class BasePoint
Public Property X As Integer
Public Property Y As Integer
End Class
Public Class ThreeDimensionalPoint
Inherits BasePoint
Public Property Z As Integer
End Class
W poprzednim kodzie JsonPolymorphic
atrybut konfiguruje TypeDiscriminatorPropertyName
wartość ."$discriminator"
Po skonfigurowaniu nazwy dyskryminującej typu w poniższym przykładzie pokazano ThreeDimensionalPoint
typ serializowany jako JSON:
BasePoint point = new ThreeDimensionalPoint { X = 1, Y = 2, Z = 3 };
var json = JsonSerializer.Serialize<BasePoint>(point);
Console.WriteLine(json);
// Sample output:
// { "$discriminator": "3d", "X": 1, "Y": 2, "Z": 3 }
Dim point As BasePoint = New ThreeDimensionalPoint With { .X = 1, .Y = 2, .Z = 3 }
Dim json As String = JsonSerializer.Serialize(Of BasePoint)(point)
Console.WriteLine(json)
' Sample output:
' { "$discriminator": "3d", "X": 1, "Y": 2, "Z": 3 }
Napiwek
Unikaj używania elementu, który JsonPolymorphicAttribute.TypeDiscriminatorPropertyName powoduje konflikt z właściwością w hierarchii typów.
Obsługa nieznanych typów pochodnych
Aby obsłużyć nieznane typy pochodne, należy wyrazić zgodę na taką obsługę przy użyciu adnotacji w typie podstawowym. Rozważmy następującą hierarchię typów:
[JsonDerivedType(typeof(ThreeDimensionalPoint))]
public class BasePoint
{
public int X { get; set; }
public int Y { get; set; }
}
public class ThreeDimensionalPoint : BasePoint
{
public int Z { get; set; }
}
public class FourDimensionalPoint : ThreeDimensionalPoint
{
public int W { get; set; }
}
<JsonDerivedType(GetType(ThreeDimensionalPoint))>
Public Class BasePoint
Public Property X As Integer
Public Property Y As Integer
End Class
Public Class ThreeDimensionalPoint
Inherits BasePoint
Public Property Z As Integer
End Class
Public NotInheritable Class FourDimensionalPoint
Inherits ThreeDimensionalPoint
Public Property W As Integer
End Class
Ponieważ konfiguracja nie wyraża jawnej zgody na FourDimensionalPoint
obsługę programu , próba serializacji wystąpień programu FourDimensionalPoint
w ten BasePoint
sposób spowoduje wyjątek czasu wykonywania:
JsonSerializer.Serialize<BasePoint>(new FourDimensionalPoint()); // throws NotSupportedException
JsonSerializer.Serialize(Of BasePoint)(New FourDimensionalPoint()) ' throws NotSupportedException
Domyślne zachowanie można zmienić przy użyciu wyliczenia JsonUnknownDerivedTypeHandling , które można określić w następujący sposób:
[JsonPolymorphic(
UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)]
[JsonDerivedType(typeof(ThreeDimensionalPoint))]
public class BasePoint
{
public int X { get; set; }
public int Y { get; set; }
}
public class ThreeDimensionalPoint : BasePoint
{
public int Z { get; set; }
}
public class FourDimensionalPoint : ThreeDimensionalPoint
{
public int W { get; set; }
}
<JsonPolymorphic(
UnknownDerivedTypeHandling:=JsonUnknownDerivedTypeHandling.FallBackToBaseType)>
<JsonDerivedType(GetType(ThreeDimensionalPoint))>
Public Class BasePoint
Public Property X As Integer
Public Property Y As Integer
End Class
Public Class ThreeDimensionalPoint
Inherits BasePoint
Public Property Z As Integer
End Class
Public NotInheritable Class FourDimensionalPoint
Inherits ThreeDimensionalPoint
Public Property W As Integer
End Class
Zamiast wracać do typu podstawowego, możesz użyć FallBackToNearestAncestor
ustawienia , aby powrócić do kontraktu najbliższego zadeklarowanego typu pochodnego:
[JsonPolymorphic(
UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToNearestAncestor)]
[JsonDerivedType(typeof(BasePoint))]
public interface IPoint { }
public class BasePoint : IPoint { }
public class ThreeDimensionalPoint : BasePoint { }
<JsonPolymorphic(
UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToNearestAncestor)>
<JsonDerivedType(GetType(BasePoint)>
Public Interface IPoint
End Interface
Public Class BasePoint
Inherits IPoint
End Class
Public Class ThreeDimensionalPoint
Inherits BasePoint
End Class
W przypadku konfiguracji podobnej do powyższego przykładu ThreeDimensionalPoint
typ będzie serializowany jako BasePoint
:
// Serializes using the contract for BasePoint
JsonSerializer.Serialize<IPoint>(new ThreeDimensionalPoint());
' Serializes using the contract for BasePoint
JsonSerializer.Serialize(Of IPoint)(New ThreeDimensionalPoint())
Jednak powrót do najbliższego przodka przyznaje możliwość "diamentu" niejednoznaczności. Rozważmy następującą hierarchię typów jako przykład:
[JsonPolymorphic(
UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToNearestAncestor)]
[JsonDerivedType(typeof(BasePoint))]
[JsonDerivedType(typeof(IPointWithTimeSeries))]
public interface IPoint { }
public interface IPointWithTimeSeries : IPoint { }
public class BasePoint : IPoint { }
public class BasePointWithTimeSeries : BasePoint, IPointWithTimeSeries { }
<JsonPolymorphic(
UnknownDerivedTypeHandling:=JsonUnknownDerivedTypeHandling.FallBackToNearestAncestor)>
<JsonDerivedType(GetType(BasePoint))>
<JsonDerivedType(GetType(IPointWithTimeSeries))>
Public Interface IPoint
End Interface
Public Interface IPointWithTimeSeries
Inherits IPoint
End Interface
Public Class BasePoint
Implements IPoint
End Class
Public Class BasePointWithTimeSeries
Inherits BasePoint
Implements IPointWithTimeSeries
End Class
W takim przypadku BasePointWithTimeSeries
typ może być serializowany jako BasePoint
albo albo IPointWithTimeSeries
ponieważ są one bezpośrednimi przodkami. Ta niejednoznaczność spowoduje NotSupportedException , że element zostanie zgłoszony podczas próby serializacji wystąpienia BasePointWithTimeSeries
jako IPoint
.
// throws NotSupportedException
JsonSerializer.Serialize<IPoint>(new BasePointWithTimeSeries());
' throws NotSupportedException
JsonSerializer.Serialize(Of IPoint)(New BasePointWithTimeSeries())
Konfigurowanie polimorfizmu przy użyciu modelu kontraktu
W przypadku przypadków użycia, w których adnotacje atrybutów są niepraktyczne lub niemożliwe (takie jak duże modele domen, hierarchie między zestawami lub hierarchie w zależnościach innych firm), aby skonfigurować polimorfizm przy użyciu modelu kontraktu. Model kontraktu to zestaw interfejsów API, które mogą służyć do konfigurowania polimorfizmu w hierarchii typów przez utworzenie niestandardowej DefaultJsonTypeInfoResolver podklasy, która dynamicznie zapewnia konfigurację polimorficzną na typ, jak pokazano w poniższym przykładzie:
public class PolymorphicTypeResolver : DefaultJsonTypeInfoResolver
{
public override JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions options)
{
JsonTypeInfo jsonTypeInfo = base.GetTypeInfo(type, options);
Type basePointType = typeof(BasePoint);
if (jsonTypeInfo.Type == basePointType)
{
jsonTypeInfo.PolymorphismOptions = new JsonPolymorphismOptions
{
TypeDiscriminatorPropertyName = "$point-type",
IgnoreUnrecognizedTypeDiscriminators = true,
UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization,
DerivedTypes =
{
new JsonDerivedType(typeof(ThreeDimensionalPoint), "3d"),
new JsonDerivedType(typeof(FourDimensionalPoint), "4d")
}
};
}
return jsonTypeInfo;
}
}
Public Class PolymorphicTypeResolver
Inherits DefaultJsonTypeInfoResolver
Public Overrides Function GetTypeInfo(
ByVal type As Type,
ByVal options As JsonSerializerOptions) As JsonTypeInfo
Dim jsonTypeInfo As JsonTypeInfo = MyBase.GetTypeInfo(type, options)
Dim basePointType As Type = GetType(BasePoint)
If jsonTypeInfo.Type = basePointType Then
jsonTypeInfo.PolymorphismOptions = New JsonPolymorphismOptions With {
.TypeDiscriminatorPropertyName = "$point-type",
.IgnoreUnrecognizedTypeDiscriminators = True,
.UnknownDerivedTypeHandling =
JsonUnknownDerivedTypeHandling.FailSerialization
}
jsonTypeInfo.PolymorphismOptions.DerivedTypes.Add(
New JsonDerivedType(GetType(ThreeDimensionalPoint), "3d"))
jsonTypeInfo.PolymorphismOptions.DerivedTypes.Add(
New JsonDerivedType(GetType(FourDimensionalPoint), "4d"))
End If
Return jsonTypeInfo
End Function
End Class
Dodatkowe szczegóły serializacji polimorficznej
- Serializacja polimorficzna obsługuje typy pochodne, które zostały jawnie wyrażeniu zgody za pośrednictwem klasy JsonDerivedTypeAttribute. Niezdecydowane typy spowodują wyjątek czasu wykonywania. Zachowanie można zmienić, konfigurując JsonPolymorphicAttribute.UnknownDerivedTypeHandling właściwość .
- Konfiguracja polimorficzna określona w typach pochodnych nie jest dziedziczona przez konfigurację polimorficzną w typach bazowych. Typ podstawowy musi być skonfigurowany niezależnie.
- Hierarchie polimorficzne są obsługiwane zarówno dla
interface
typów, jak iclass
. - Polimorfizm przy użyciu dyskryminujących typów jest obsługiwany tylko w przypadku hierarchii typów, które używają domyślnych konwerterów dla obiektów, kolekcji i typów słowników.
- Polimorfizm jest obsługiwany w generowaniu źródła opartym na metadanych, ale nie w przypadku generowania źródła szybkiej ścieżki.