Partager via


Comment sérialiser les propriétés des classes dérivées avec System.Text.Json

Dans cet article, vous apprendrez à sérialiser les propriétés des classes dérivées avec le namespace System.Text.Json.

Sérialiser les propriétés des classes dérivées

À compter de .NET 7, System.Text.Json prend en charge la sérialisation et la désérialisation des hiérarchies de types polymorphes avec des annotations d’attributs.

Attribut Description
JsonDerivedTypeAttribute Lorsqu’il est placé sur une déclaration de type, indique que le sous-type spécifié doit être choisi dans la sérialisation polymorphe. Il expose également la possibilité de spécifier un discriminateur de type.
JsonPolymorphicAttribute Lorsqu’il est placé sur une déclaration de type, indique que le type doit être sérialisé de façon polymorphe. Il expose également différentes options pour configurer la sérialisation et la désérialisation polymorphes pour ce type.

Par exemple, supposons que vous avez une classe WeatherForecastBase et une classe dérivée 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

Et supposons que l’argument de type de la méthode Serialize<TValue> au moment de la compilation est WeatherForecastBase :

options = new JsonSerializerOptions
{
    WriteIndented = true
};
jsonString = JsonSerializer.Serialize<WeatherForecastBase>(weatherForecastBase, options);
options = New JsonSerializerOptions With {
    .WriteIndented = True
}
jsonString = JsonSerializer.Serialize(WeatherForecastBase, options)

Dans ce scénario, la propriété City est sérialisée parce que l’objet weatherForecastBase est en fait un objet WeatherForecastWithCity. Cette configuration autorise la sérialisation polymorphe pour WeatherForecastBase, en particulier lorsque le type de runtime est WeatherForecastWithCity :

{
  "City": "Milwaukee",
  "Date": "2022-09-26T00:00:00-05:00",
  "TemperatureCelsius": 15,
  "Summary": "Cool"
}

Même si l’aller-retour de la charge utile comme WeatherForecastBase est pris en charge, il ne se matérialise pas en tant que type de runtime de WeatherForecastWithCity. Au lieu de cela, il se matérialise en tant que type de runtime 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

La section suivante explique comment ajouter des métadonnées pour activer l’aller-retour du type dérivé.

Discriminateurs de type polymorphe

Pour activer la désérialisation polymorphe, vous devez spécifier un discriminateur de type pour la classe dérivée :

[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

Avec les métadonnées ajoutées, plus précisément, le discriminateur de type, le sérialiseur peut sérialiser et désérialiser la charge utile en tant que type WeatherForecastWithCity à partir de son type de base WeatherForecastBase. La sérialisation émet du JSON avec les métadonnées du discriminant de type :

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"
'   }

Avec le discriminateur de type, le sérialiseur peut désérialiser la charge utile de façon polymorphe comme 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

Remarque

Par défaut, le discriminant $type doit être placé au début de l’objet JSON, regroupé avec d’autres propriétés de métadonnées comme $id et $ref. Si vous lisez des données provenant d’une API externe qui place le discriminant $type au milieu de l’objet JSON, définissez JsonSerializerOptions.AllowOutOfOrderMetadataProperties sur true :

JsonSerializerOptions options = new() { AllowOutOfOrderMetadataProperties = true };
JsonSerializer.Deserialize<Base>("""{"Name":"Name","$type":"derived"}""", options);

Soyez prudent lorsque vous activez cet indicateur, car cela pourrait entraîner un surdimensionnement des tampons (et des échecs de mémoire) lors de la désérialisation en flux de très gros objets JSON.

Combiner et mettre en correspondance les formats de discriminateur de type

Les identificateurs de discriminateur de type sont valides sous la forme string ou int de sorte que les éléments suivants sont valides :

[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...
'  }

Bien que l’API prenne en charge les combinaisons et correspondances des configurations de discriminateur de type, elle n’est pas recommandée. La recommandation générale est d’utiliser tous les discriminateurs de type string, tous les discriminateurs de type int, ou aucun discriminateur du tout. L’exemple suivant montre comment combiner et mettre en correspondance des configurations de discriminateur de type :

[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

Dans l’exemple précédent, le type BasePoint n’a pas de discriminateur de type, tandis que le type ThreeDimensionalPoint a un discriminateur de type int et le type FourDimensionalPoint a un discriminateur de type string.

Important

Pour que la sérialisation polymorphe fonctionne, le type de la valeur sérialisée doit être celui du type de base polymorphe. Cela comprend l’utilisation du type de base comme paramètre de type générique lors de la sérialisation des valeurs de niveau racine, comme type déclaré de propriétés sérialisées ou comme élément de collection dans les collections sérialisées.

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

Personnaliser le nom du discriminateur de type

Le nom de propriété par défaut du discriminateur de type est $type. Pour personnaliser le nom de la propriété, utilisez JsonPolymorphicAttribute comme indiqué dans l’exemple suivant :

[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

Dans le code précédent, l’attribut JsonPolymorphic configure le TypeDiscriminatorPropertyName avec la valeur "$discriminator". Une fois le nom du discriminateur de type configuré, l’exemple suivant montre le type ThreeDimensionalPoint sérialisé au format 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 }

Conseil

Évitez d’utiliser JsonPolymorphicAttribute.TypeDiscriminatorPropertyName s’il est en conflit avec une propriété dans votre hiérarchie de types.

Gérer des types dérivés inconnus

Pour gérer les types dérivés inconnus, vous devez choisir une telle prise en charge au moyen d’une annotation sur le type de base. Prenez en considération la hiérarchie de types suivante :

[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

Étant donné que la configuration n’accepte pas explicitement la prise en charge de FourDimensionalPoint, la tentative de sérialisation des instances de FourDimensionalPoint comme BasePoint entraîne une exception de runtime :

JsonSerializer.Serialize<BasePoint>(new FourDimensionalPoint()); // throws NotSupportedException
JsonSerializer.Serialize(Of BasePoint)(New FourDimensionalPoint()) ' throws NotSupportedException

Vous pouvez changer le comportement par défaut en utilisant l’enum JsonUnknownDerivedTypeHandling, qui peut être spécifié comme suit :

[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

Au lieu de revenir au type de base, vous pouvez utiliser le paramètre FallBackToNearestAncestor pour revenir au contrat du type dérivé déclaré le plus proche :

[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

Avec une configuration comme dans l’exemple précédent, le type ThreeDimensionalPoint est sérialisé comme BasePoint :

// Serializes using the contract for BasePoint
JsonSerializer.Serialize<IPoint>(new ThreeDimensionalPoint());
' Serializes using the contract for BasePoint
JsonSerializer.Serialize(Of IPoint)(New ThreeDimensionalPoint())

Cependant, revenir à l’ancêtre le plus proche admet la possibilité d’une ambiguïté « diamant ». Prenez en considération la hiérarchie de types suivante comme exemple :

[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

Dans ce cas, le type BasePointWithTimeSeries peut être sérialisé comme BasePoint ou IPointWithTimeSeries parce qu’ils sont tous deux des ancêtres directs. Cette ambiguïté entraîne la levée de NotSupportedException lors de la tentative de sérialisation d’une instance de BasePointWithTimeSeries comme IPoint.

// throws NotSupportedException
JsonSerializer.Serialize<IPoint>(new BasePointWithTimeSeries());
' throws NotSupportedException
JsonSerializer.Serialize(Of IPoint)(New BasePointWithTimeSeries())

Configurer le polymorphisme avec le modèle de contrat

Pour les cas d’usage où les annotations d’attribut sont impraticables ou impossibles (par exemple, les grands modèles de domaine, les hiérarchies inter-assemblys ou les hiérarchies dans des dépendances tierces), pour configurer le polymorphisme, utilisez le modèle de contrat. Le modèle de contrat est un ensemble d’API qui peuvent être utilisées pour configurer le polymorphisme dans une hiérarchie de types en créant une sous-classe DefaultJsonTypeInfoResolver personnalisée qui fournit dynamiquement une configuration polymorphe par type, comme illustré dans l’exemple suivant :

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

Détails supplémentaires sur la sérialisation polymorphe

  • La sérialisation polymorphe prend en charge les types dérivés qui ont été explicitement activés via JsonDerivedTypeAttribute. Les types non déclarés entraînent une exception de runtime. Ce comportement peut être modifié en configurant la propriété JsonPolymorphicAttribute.UnknownDerivedTypeHandling.
  • La configuration polymorphe spécifiée dans les types dérivés n’est pas héritée par la configuration polymorphe dans les types de base. Le type de base doit être configuré indépendamment.
  • Les hiérarchies polymorphes sont prises en charge pour les types interface et class.
  • Le polymorphisme utilisant des discriminateurs de type est pris en charge uniquement pour les hiérarchies de types qui utilisent les convertisseurs par défaut pour les objets, les collections et les types de dictionnaires.
  • Le polymorphisme est pris en charge dans la génération de source basée sur les métadonnées, mais pas dans la génération de source à chemin rapide.

Voir aussi