Come serializzare le proprietà delle classi derivate con System.Text.Json
Questo articolo illustra come serializzare le proprietà delle classi derivate con lo spazio dei System.Text.Json
nomi .
Serializzare le proprietà delle classi derivate
A partire da .NET 7, System.Text.Json
supporta la serializzazione e la deserializzazione della gerarchia di tipi polimorfici con annotazioni degli attributi.
Attributo | Descrizione |
---|---|
JsonDerivedTypeAttribute | Se posizionato su una dichiarazione di tipo, indica che il sottotipo specificato deve essere scelto per la serializzazione polimorfica. Espone anche la possibilità di specificare un discriminatore di tipo. |
JsonPolymorphicAttribute | Se inserito in una dichiarazione di tipo, indica che il tipo deve essere serializzato in modo polimorfico. Espone anche varie opzioni per configurare la serializzazione e la deserializzazione polimorfica per tale tipo. |
Si supponga, ad esempio, di avere una classe WeatherForecastBase
e una classe derivata WeatherForecastWithCity
:
[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
Si supponga che l'argomento tipo del metodo Serialize<TValue>
in fase di compilazione sia WeatherForecastBase
:
options = new JsonSerializerOptions
{
WriteIndented = true
};
jsonString = JsonSerializer.Serialize<WeatherForecastBase>(weatherForecastBase, options);
options = New JsonSerializerOptions With {
.WriteIndented = True
}
jsonString = JsonSerializer.Serialize(WeatherForecastBase, options)
In questo scenario, la proprietà City
viene serializzata perché l'oggetto weatherForecastBase
è effettivamente un oggetto WeatherForecastWithCity
. Questa configurazione abilita la serializzazione polimorfica per WeatherForecastBase
, in particolare quando il tipo di runtime è WeatherForecastWithCity
:
{
"City": "Milwaukee",
"Date": "2022-09-26T00:00:00-05:00",
"TemperatureCelsius": 15,
"Summary": "Cool"
}
Anche se il round trip del payload come WeatherForecastBase
è supportato, non si materializzerà come tipo di runtime di WeatherForecastWithCity
. Si materializzerà invece come tipo di runtime di WeatherForecastBase
:
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
Nella sezione seguente viene descritto come aggiungere i metadati per consentire il round trip del tipo derivato.
Discriminanti di tipi polimorfici
Per abilitare la deserializzazione polimorfica, è necessario specificare un discriminatore di tipo per la classe derivata:
[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
Con i metadati aggiunti, in particolare il discriminatore di tipo, il serializzatore può serializzare e deserializzare il payload come tipo WeatherForecastWithCity
dal suo tipo di base WeatherForecastBase
. La serializzazione genera JSON insieme ai metadati discriminatori del tipo:
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"
' }
Con il discriminante di tipo, il serializzatore può deserializzare il payload in modo polimorfico come 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
Nota
Per impostazione predefinita, il $type
discriminare deve essere posizionato all'inizio dell'oggetto JSON, raggruppato con altre proprietà di metadati come $id
e $ref
. Se si legge i dati da un'API esterna che inserisce il $type
discriminante al centro dell'oggetto JSON, impostare su JsonSerializerOptions.AllowOutOfOrderMetadataProperties true
:
JsonSerializerOptions options = new() { AllowOutOfOrderMetadataProperties = true };
JsonSerializer.Deserialize<Base>("""{"Name":"Name","$type":"derived"}""", options);
Prestare attenzione quando si abilita questo flag, in quanto potrebbe comportare errori di over buffering (e errori di memoria insufficiente) durante l'esecuzione della deserializzazione di flussi di oggetti JSON di dimensioni molto grandi.
Combinazione e corrispondenza dei formati dei discriminatori di tipo
Gli identificatori del discriminatore di tipo sono validi nei moduli string
o int
, pertanto è valido quanto segue:
[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...
' }
Anche se l'API supporta la combinazione e la corrispondenza delle configurazioni discriminatori di tipo, non è consigliabile. La raccomandazione generale è quella di usare tutti i string
discriminanti di tipo, tutti i int
discriminanti di tipo o nessun discriminatorio. L'esempio seguente mostra come combinare e associare le configurazioni dei discriminatori di tipo:
[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
Nell'esempio precedente, il tipo BasePoint
non dispone di un discriminante di tipo, mentre il tipo ThreeDimensionalPoint
ha un discriminante di tipo int
e FourDimensionalPoint
ha un discriminante di tipo string
.
Importante
Per il funzionamento della serializzazione polimorfica, il tipo del valore serializzato deve essere quello del tipo di base polimorfico. Ciò include l'uso del tipo di base come parametro di tipo generico durante la serializzazione dei valori a livello radice, come tipo dichiarato di proprietà serializzate o come elemento della raccolta nelle raccolte serializzate.
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
Personalizzare il nome del discriminatore di tipo
Il nome predefinito della proprietà per il discriminatore di tipo è $type
. Per personalizzare il nome della proprietà, usare JsonPolymorphicAttribute come mostrato nell'esempio seguente:
[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
Nel codice precedente, l'attributo JsonPolymorphic
configura TypeDiscriminatorPropertyName
sul valore "$discriminator"
. Con il nome del discriminatore di tipo configurato, l'esempio seguente mostra il tipo ThreeDimensionalPoint
serializzato come 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 }
Suggerimento
Evitare di usare un JsonPolymorphicAttribute.TypeDiscriminatorPropertyName in conflitto con una proprietà nella gerarchia dei tipi.
Gestire i tipi derivati sconosciuti
Per gestire i tipi derivati sconosciuti, è necessario acconsentire esplicitamente al loro supporto con un'annotazione sul tipo di base. Si consideri la gerarchia di tipi seguente:
[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
Poiché la configurazione non supporta esplicitamente il supporto per FourDimensionalPoint
, il tentativo di serializzare le istanze di FourDimensionalPoint
come BasePoint
genererà un'eccezione di runtime:
JsonSerializer.Serialize<BasePoint>(new FourDimensionalPoint()); // throws NotSupportedException
JsonSerializer.Serialize(Of BasePoint)(New FourDimensionalPoint()) ' throws NotSupportedException
È possibile modificare il comportamento predefinito usando l'enumerazione JsonUnknownDerivedTypeHandling, che può essere specificata nel modo seguente:
[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
Anziché eseguire il fallback al tipo di base, è possibile usare l'impostazione FallBackToNearestAncestor
per eseguire il fallback al contratto del tipo derivato dichiarato più vicino:
[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
Con una configurazione come quella dell'esempio precedente, il tipo ThreeDimensionalPoint
verrà serializzato come BasePoint
:
// Serializes using the contract for BasePoint
JsonSerializer.Serialize<IPoint>(new ThreeDimensionalPoint());
' Serializes using the contract for BasePoint
JsonSerializer.Serialize(Of IPoint)(New ThreeDimensionalPoint())
Tuttavia, il fallback al predecessore più vicino ammette la possibilità di ambiguità "diamante". Si consideri la seguente gerarchia di tipi come esempio:
[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
In questo caso, il tipo BasePointWithTimeSeries
può essere serializzato come BasePoint
o IPointWithTimeSeries
poiché sono entrambi predecessori diretti. Questa ambiguità causerà la generazione di NotSupportedException quando si tenta di serializzare un'istanza BasePointWithTimeSeries
come IPoint
.
// throws NotSupportedException
JsonSerializer.Serialize<IPoint>(new BasePointWithTimeSeries());
' throws NotSupportedException
JsonSerializer.Serialize(Of IPoint)(New BasePointWithTimeSeries())
Configurare il polimorfismo con il modello di contratto
Per i casi d'uso in cui le annotazioni degli attributi sono poco pratiche o impossibili (ad esempio modelli di dominio di grandi dimensioni, gerarchie tra assembly o gerarchie nelle dipendenze di terze parti), per configurare il polimorfismo usare il modello di contratto. Il modello di contratto è un set di API che possono essere usate per configurare il polimorfismo in una gerarchia di tipi creando una sottoclasse personalizzata DefaultJsonTypeInfoResolver che fornisce dinamicamente una configurazione polimorfica per tipo, come illustrato nell'esempio seguente:
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
Dettagli aggiuntivi sulla serializzazione polimorfica
- La serializzazione polimorfica supporta i tipi derivati esplicitamente accodati tramite JsonDerivedTypeAttribute. I tipi non dichiarati genereranno un'eccezione di runtime. Il comportamento può essere modificato configurando la proprietà JsonPolymorphicAttribute.UnknownDerivedTypeHandling.
- La configurazione polimorfica specificata nei tipi derivati non viene ereditata dalla configurazione polimorfica nei tipi di base. Il tipo di base deve essere configurato in modo indipendente.
- Le gerarchie polimorfiche sono supportate sia per
interface
che per i tipiclass
. - Il polimorfismo che usa i discriminatori dei tipi è supportato solo per le gerarchie di tipi che usano i convertitori predefiniti per oggetti, raccolte e tipi di dizionario.
- Il polimorfismo è supportato nella generazione di origine basata su metadati, ma non nella generazione di origini fast-path.