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

Dans les versions antérieures à .NET 7, System.Text.Json ne prend pas en charge la sérialisation des hiérarchies de types polymorphes. Par exemple, si le type d’une propriété est une interface ou une classe abstraite, seules les propriétés définies sur l’interface ou la classe abstraite sont sérialisées, même si le type de runtime a des propriétés supplémentaires. Les exceptions à ce comportement sont expliquées dans cette section. Pour plus d’informations sur la prise en charge dans .NET 7, consultez Sérialisation polymorphe dans .NET 7.

Par exemple, supposons que vous avez une classe WeatherForecast et une classe dérivée WeatherForecastDerived :

public class WeatherForecast
{
    public DateTimeOffset Date { get; set; }
    public int TemperatureCelsius { get; set; }
    public string? Summary { get; set; }
}
Public Class WeatherForecast
    Public Property [Date] As DateTimeOffset
    Public Property TemperatureCelsius As Integer
    Public Property Summary As String
End Class
public class WeatherForecastDerived : WeatherForecast
{
    public int WindSpeed { get; set; }
}
Public Class WeatherForecastDerived
    Inherits WeatherForecast
    Public Property WindSpeed As Integer
End Class

Et supposons que l’argument de type de la méthode Serialize au moment de la compilation est WeatherForecast :

var options = new JsonSerializerOptions
{
    WriteIndented = true
};
jsonString = JsonSerializer.Serialize<WeatherForecast>(weatherForecast, options);
Dim options As JsonSerializerOptions = New JsonSerializerOptions With {
    .WriteIndented = True
}
jsonString = JsonSerializer.Serialize(weatherForecast1, options)

Dans ce scénario, la propriété WindSpeed n’est pas sérialisée même si l’objet weatherForecast est un objet WeatherForecastDerived. Seules les propriétés de classe de base sont sérialisées :

{
  "Date": "2019-08-01T00:00:00-07:00",
  "TemperatureCelsius": 25,
  "Summary": "Hot"
}

Ce comportement est destiné à empêcher l’exposition accidentelle de données dans un type dérivé créé au runtime.

Pour sérialiser les propriétés du type dérivé dans l’exemple précédent, utilisez l’une des approches suivantes :

  • Appelez une surcharge de Serialize qui vous permet de spécifier le type au moment du runtime :

    options = new JsonSerializerOptions
    {
        WriteIndented = true
    };
    jsonString = JsonSerializer.Serialize(weatherForecast, weatherForecast.GetType(), options);
    
    options = New JsonSerializerOptions With {
        .WriteIndented = True
    }
    jsonString = JsonSerializer.Serialize(weatherForecast1, weatherForecast1.[GetType](), options)
    
  • Déclarez l’objet à sérialiser comme object.

    options = new JsonSerializerOptions
    {
        WriteIndented = true
    };
    jsonString = JsonSerializer.Serialize<object>(weatherForecast, options);
    
    options = New JsonSerializerOptions With {
        .WriteIndented = True
    }
    jsonString = JsonSerializer.Serialize(Of Object)(weatherForecast1, options)
    

Dans l’exemple de scénario précédent, les deux approches entraînent l’inclusion de la propriété WindSpeed dans la sortie JSON :

{
  "WindSpeed": 35,
  "Date": "2019-08-01T00:00:00-07:00",
  "TemperatureCelsius": 25,
  "Summary": "Hot"
}

Important

Ces approches fournissent une sérialisation polymorphe uniquement pour l’objet racine à sérialiser, pas pour les propriétés de cet objet racine.

Vous pouvez obtenir une sérialisation polymorphe pour les objets de niveau inférieur si vous les définissez comme type object. Par exemple, supposons que votre classe WeatherForecast a une propriété nommée PreviousForecast qui peut être définie comme type WeatherForecast ou object :

public class WeatherForecastWithPrevious
{
    public DateTimeOffset Date { get; set; }
    public int TemperatureCelsius { get; set; }
    public string? Summary { get; set; }
    public WeatherForecast? PreviousForecast { get; set; }
}
Public Class WeatherForecastWithPrevious
    Public Property [Date] As DateTimeOffset
    Public Property TemperatureCelsius As Integer
    Public Property Summary As String
    Public Property PreviousForecast As WeatherForecast
End Class
public class WeatherForecastWithPreviousAsObject
{
    public DateTimeOffset Date { get; set; }
    public int TemperatureCelsius { get; set; }
    public string? Summary { get; set; }
    public object? PreviousForecast { get; set; }
}
Public Class WeatherForecastWithPreviousAsObject
    Public Property [Date] As DateTimeOffset
    Public Property TemperatureCelsius As Integer
    Public Property Summary As String
    Public Property PreviousForecast As Object
End Class

Si la propriété PreviousForecast contient une instance de WeatherForecastDerived :

  • La sortie JSON de la sérialisation WeatherForecastWithPrevious n’inclut pas WindSpeed.
  • La sortie JSON de la sérialisation WeatherForecastWithPreviousAsObject inclut WindSpeed.

Pour sérialiser WeatherForecastWithPreviousAsObject, il n’est pas nécessaire d’appeler Serialize<object> ou GetType parce que l’objet racine n’est pas celui qui peut être d’un type dérivé. L’exemple de code suivant n’appelle pas Serialize<object> ou GetType :

options = new JsonSerializerOptions
{
    WriteIndented = true
};
jsonString = JsonSerializer.Serialize(weatherForecastWithPreviousAsObject, options);
options = New JsonSerializerOptions With {
    .WriteIndented = True
}
jsonString = JsonSerializer.Serialize(weatherForecastWithPreviousAsObject1, options)

Le code précédent sérialise WeatherForecastWithPreviousAsObjectcorrectement :

{
  "Date": "2019-08-01T00:00:00-07:00",
  "TemperatureCelsius": 25,
  "Summary": "Hot",
  "PreviousForecast": {
    "WindSpeed": 35,
    "Date": "2019-08-01T00:00:00-07:00",
    "TemperatureCelsius": 25,
    "Summary": "Hot"
  }
}

La même approche de la définition des propriétés que object fonctionne avec les interfaces. Supposons que vous avez l’interface et l’implémentation suivantes, et que vous souhaitez sérialiser une classe avec des propriétés qui contiennent des instances d’implémentation :

namespace SystemTextJsonSamples
{
    public interface IForecast
    {
        public DateTimeOffset Date { get; set; }
        public int TemperatureCelsius { get; set; }
        public string? Summary { get; set; }
    }

    public class Forecast : IForecast
    {
        public DateTimeOffset Date { get; set; }
        public int TemperatureCelsius { get; set; }
        public string? Summary { get; set; }
        public int WindSpeed { get; set; }
    }

    public class Forecasts
    {
        public IForecast? Monday { get; set; }
        public object? Tuesday { get; set; }
    }
}
Namespace SystemTextJsonSamples

    Public Interface IForecast
        Property [Date] As DateTimeOffset
        Property TemperatureCelsius As Integer
        Property Summary As String
    End Interface

    Public Class Forecast
        Implements IForecast
        Public Property [Date] As DateTimeOffset Implements IForecast.[Date]
        Public Property TemperatureCelsius As Integer Implements IForecast.TemperatureCelsius
        Public Property Summary As String Implements IForecast.Summary
        Public Property WindSpeed As Integer
    End Class

    Public Class Forecasts
        Public Property Monday As IForecast
        Public Property Tuesday As Object
    End Class

End Namespace

Lorsque vous sérialisez une instance de Forecasts, seul Tuesday montre la propriété WindSpeed parce que Tuesday est défini comme object :

var forecasts = new Forecasts
{
    Monday = new Forecast
    {
        Date = DateTime.Parse("2020-01-06"),
        TemperatureCelsius = 10,
        Summary = "Cool",
        WindSpeed = 8
    },
    Tuesday = new Forecast
    {
        Date = DateTime.Parse("2020-01-07"),
        TemperatureCelsius = 11,
        Summary = "Rainy",
        WindSpeed = 10
    }
};

options = new JsonSerializerOptions
{
    WriteIndented = true
};
jsonString = JsonSerializer.Serialize(forecasts, options);
Dim forecasts1 As New Forecasts With {
    .Monday = New Forecast With {
        .[Date] = Date.Parse("2020-01-06"),
        .TemperatureCelsius = 10,
        .Summary = "Cool",
        .WindSpeed = 8
    },
    .Tuesday = New Forecast With {
        .[Date] = Date.Parse("2020-01-07"),
        .TemperatureCelsius = 11,
        .Summary = "Rainy",
        .WindSpeed = 10
    }
}

options = New JsonSerializerOptions With {
    .WriteIndented = True
}
jsonString = JsonSerializer.Serialize(forecasts1, options)

L’exemple suivant montre le JSON qui résulte du code précédent :

{
  "Monday": {
    "Date": "2020-01-06T00:00:00-08:00",
    "TemperatureCelsius": 10,
    "Summary": "Cool"
  },
  "Tuesday": {
    "Date": "2020-01-07T00:00:00-08:00",
    "TemperatureCelsius": 11,
    "Summary": "Rainy",
    "WindSpeed": 10
  }
}

Notes

Cet article traite de la sérialisation, pas de la désérialisation. La désérialisation polymorphe n’est pas prise en charge dans les versions antérieures à .NET 7, mais comme solution de contournement, vous pouvez écrire un convertisseur personnalisé, comme l’exemple dans Prise en charge de la désérialisation polymorphe. Pour plus d’informations sur la façon dont .NET 7 prend en charge la sérialisation et la désérialisation polymorphes, consultez Guide pratique pour sérialiser les propriétés des classes dérivées avec System.Text.Json dans .NET 7.

À 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