Serialización de propiedades de clases derivadas con System.Text.Json
En este artículo, aprenderá a serializar las propiedades de las clases derivadas con el espacio de nombres System.Text.Json
.
Serialización de propiedades de clases derivadas
A partir de .NET 7, System.Text.Json
admite la serialización y deserialización de la jerarquía de tipos polimórficos con anotaciones de atributos.
Atributo | Descripción |
---|---|
JsonDerivedTypeAttribute | Cuando se coloca en una declaración de tipo, indica que el subtipo especificado debe participar en la serialización polimórfica. También expone la posibilidad de especificar un discriminador de tipos. |
JsonPolymorphicAttribute | Cuando se coloca en una declaración de tipo, indica que el tipo se debe serializar polimórficamente. También expone varias opciones para configurar la serialización y deserialización polimórficas para ese tipo. |
Por ejemplo, supongamos que tiene una clase WeatherForecastBase
y una clase derivada 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
Supongamos también que el argumento de tipo del método Serialize<TValue>
en tiempo de compilación es WeatherForecastBase
:
options = new JsonSerializerOptions
{
WriteIndented = true
};
jsonString = JsonSerializer.Serialize<WeatherForecastBase>(weatherForecastBase, options);
options = New JsonSerializerOptions With {
.WriteIndented = True
}
jsonString = JsonSerializer.Serialize(WeatherForecastBase, options)
En este escenario, la propiedad City
no se serializa porque el objeto weatherForecastBase
es en realidad un objeto WeatherForecastWithCity
. Esta configuración permite la serialización polimórfica para WeatherForecastBase
, específicamente cuando el tipo en tiempo de ejecución es WeatherForecastWithCity
:
{
"City": "Milwaukee",
"Date": "2022-09-26T00:00:00-05:00",
"TemperatureCelsius": 15,
"Summary": "Cool"
}
Aunque se admite el recorrido de ida y vuelta de la carga como WeatherForecastBase
, no se materializará como un tipo en tiempo de ejecución de WeatherForecastWithCity
. En su lugar, se materializará como un tipo en tiempo de ejecución de 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
En la sección siguiente se describe cómo agregar metadatos para permitir el recorrido de ida y vuelta del tipo derivado.
Discriminadores de tipos polimórficos
Para habilitar la deserialización polimórfica, debe especificar un discriminador de tipos para la clase derivada:
[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 los metadatos agregados, en concreto, el discriminador de tipos, el serializador puede serializar y deserializar la carga como el tipo WeatherForecastWithCity
de su tipo base WeatherForecastBase
. La serialización emitirá JSON junto con los metadatos de discriminador de tipos:
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 el discriminador de tipos, el serializador puede deserializar la carga polimórficamente como 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:
Por defecto, el discriminador $type
debe colocarse al principio del objeto JSON, agrupado junto con otras propiedades de metadatos como $id
y $ref
. Si está leyendo datos de una API externa que coloca el discriminador $type
en medio del objeto JSON, active JsonSerializerOptions.AllowOutOfOrderMetadataProperties como true
:
JsonSerializerOptions options = new() { AllowOutOfOrderMetadataProperties = true };
JsonSerializer.Deserialize<Base>("""{"Name":"Name","$type":"derived"}""", options);
Tenga cuidado al habilitar esta marca, ya que podría dar lugar a errores de sobrebúfer (y errores de memoria insuficiente) al realizar la deserialización de streaming de objetos JSON muy grandes.
Combinación y coincidencia de formatos de discriminador de tipos
Los identificadores de discriminador de tipos son válidos en los formatos string
o int
, por lo que lo siguiente es válido:
[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...
' }
Aunque la API admite la combinación y coincidencia de configuraciones de discriminador de tipos, no se recomienda. La recomendación general es usar todos los discriminadores de tipos string
, todos los discriminadores de tipos int
o ningún discriminador. En el ejemplo siguiente se muestra cómo combinar y hacer coincidir las configuraciones de discriminador de tipos:
[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
En el ejemplo anterior, el tipo BasePoint
no tiene un discriminador de tipos, mientras que el tipo ThreeDimensionalPoint
tiene un discriminador de tipos int
y FourDimensionalPoint
tiene un discriminador de tipos string
.
Importante
Para que la serialización polimórfica funcione, el tipo del valor serializado debe ser el del tipo base polimórfico. Esto incluye el uso del tipo base como parámetro de tipo genérico al serializar valores de nivel raíz, como tipo declarado de propiedades serializadas o como elemento de colección en colecciones serializadas.
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
Personalización del nombre del discriminador de tipos
El nombre de propiedad predeterminado del discriminador de tipos es $type
. Para personalizar el nombre de la propiedad, use JsonPolymorphicAttribute como se muestra en el ejemplo siguiente:
[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
En el código anterior, el atributo JsonPolymorphic
configura TypeDiscriminatorPropertyName
en el valor "$discriminator"
. Con el nombre del discriminador de tipos configurado, en el ejemplo siguiente se muestra el tipo ThreeDimensionalPoint
serializado como 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 }
Sugerencia
No use JsonPolymorphicAttribute.TypeDiscriminatorPropertyName si entra en conflicto con una propiedad de la jerarquía de tipos.
Administración de tipos derivados desconocidos
Para administrar tipos derivados desconocidos, debe elegir dicha compatibilidad mediante una anotación en el tipo base. Considere la jerarquía de tipos siguiente:
[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
Dado que la configuración no admite explícitamente la compatibilidad con FourDimensionalPoint
, al intentar serializar instancias de FourDimensionalPoint
como BasePoint
se producirá una excepción en tiempo de ejecución:
JsonSerializer.Serialize<BasePoint>(new FourDimensionalPoint()); // throws NotSupportedException
JsonSerializer.Serialize(Of BasePoint)(New FourDimensionalPoint()) ' throws NotSupportedException
Puede cambiar el comportamiento predeterminado mediante la enumeración JsonUnknownDerivedTypeHandling, que se puede especificar de la siguiente manera:
[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
En lugar de retroceder al tipo base, puede usar el valor FallBackToNearestAncestor
para retroceder al contrato del tipo derivado declarado más cercano:
[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 configuración como el ejemplo anterior, el tipo ThreeDimensionalPoint
se serializará como BasePoint
:
// Serializes using the contract for BasePoint
JsonSerializer.Serialize<IPoint>(new ThreeDimensionalPoint());
' Serializes using the contract for BasePoint
JsonSerializer.Serialize(Of IPoint)(New ThreeDimensionalPoint())
Sin embargo, retroceder hasta el antepasado más próximo admite la posibilidad de ambigüedad del "diamante". Considere la siguiente jerarquía de tipos como ejemplo:
[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
En este caso, el tipo BasePointWithTimeSeries
se podría serializar como BasePoint
o IPointWithTimeSeries
dado que ambos son antecesores directos. Esta ambigüedad provocará que se inicie la excepción NotSupportedException al intentar serializar una instancia de BasePointWithTimeSeries
como IPoint
.
// throws NotSupportedException
JsonSerializer.Serialize<IPoint>(new BasePointWithTimeSeries());
' throws NotSupportedException
JsonSerializer.Serialize(Of IPoint)(New BasePointWithTimeSeries())
Configuración del polimorfismo con el modelo de contrato
En los casos de uso en los que las anotaciones de atributo no son prácticas o imposibles (como modelos de dominio grandes, jerarquías entre ensamblados o jerarquías en dependencias de terceros), para configurar el polimorfismo use el modelo de contrato. El modelo de contrato es un conjunto de API que se pueden usar para configurar el polimorfismo en una jerarquía de tipos mediante la creación de una subclase DefaultJsonTypeInfoResolver personalizada que proporciona dinámicamente una configuración polimórfica por tipo, como se muestra en el ejemplo siguiente:
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
Detalles adicionales de serialización polimórfica
- La serialización polimórfica admite tipos derivados que se han elegido explícitamente a través de JsonDerivedTypeAttribute. Los tipos no declarados producirán una excepción en tiempo de ejecución. Este comportamiento se puede cambiar configurando la propiedad JsonPolymorphicAttribute.UnknownDerivedTypeHandling.
- La configuración polimórfica en tipos base no hereda la configuración polimórfica especificada en los tipos derivados. El tipo base debe configurarse de forma independiente.
- Se admiten jerarquías polimórficas para los tipos
interface
yclass
. - El polimorfismo mediante discriminadores de tipos solo se admite para jerarquías de tipos que usan los convertidores predeterminados para objetos, colecciones y tipos de diccionario.
- El polimorfismo se admite en la generación de origen basada en metadatos, pero no en la generación de origen de ruta de acceso rápida.