語言

如何使用 System.Text.Json 序列化衍生類別的屬性

在本文中,您將瞭解如何使用 System.Text.Json 命名空間串行化衍生類別的屬性。

序列化衍生類別的屬性

從 .NET 7 開始,System.Text.Json 支援使用屬性註釋進行多型型別階層序列化和還原序列化。

屬性 描述
JsonDerivedTypeAttribute 當置於型別宣告上時,表示指定的子型別應選擇啟用多型序列化。 其也會公開指定型別鑑別子的功能。
JsonPolymorphicAttribute 放在型別宣告上時,指出型別應該以多型方式序列化。 其也會公開各種選項,以設定該型別的多型序列化和還原序列化。

例如,假設您有 WeatherForecastBase 類別和衍生類別 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

此外,假設在編譯時間,Serialize<TValue> 方法的型別引數為 WeatherForecastBase:

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

在此情況下,系統會序列化 City 屬性,因為 weatherForecastBase 物件實際上是 WeatherForecastWithCity 物件。 此設定會啟用 WeatherForecastBase 的多型序列化,特別是當執行階段型別為 WeatherForecastWithCity 時:

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

雖然有效載荷 WeatherForecastBase 的往返傳輸支援,但不會以執行時類型 WeatherForecastWithCity的形式實現。 取而代之的是,它會以執行時類型的形式呈現: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

下列章節說明如何新增中繼資料,以便支援衍生型別的來回轉換。

多型型別鑑別子

若要啟用多型還原序列化,您必須為衍生類別指定型別鑑別子:

[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

透過新增的中繼資料,具體來說就是型別鑑別子,序列化程式可以從其基底型別 WeatherForecastBase,將酬載序列化及還原序列化為 WeatherForecastWithCity 型別。 串行化會連同型別歧視性元數據一起發出 JSON:

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

使用型別鑑別子,序列化程式能以多型方式將承載還原序列化為 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

注意

根據預設, $type 歧視性必須放在 JSON 物件的開頭,並與其他元數據屬性分組,例如 $id 和 $ref。 如果您正在從外部 API 讀取資料,而該 API 會將 $type 辨別欄位放在 JSON 物件的中間,請將 JsonSerializerOptions.AllowOutOfOrderMetadataProperties 設為 true:

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

啟用此旗標時請務必小心,因為在對非常大的 JSON 物件執行串流反序列化時,可能會導致過度緩衝,甚至發生記憶體不足錯誤。

混合和比對型別鑑別子格式

型別鑑別子識別碼不論是 string 或 int 格式都有效,因此下列是有效的:

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

雖然 API 支援混用不同的型別鑑別子組態,但不建議這樣做。 一般建議是使用所有 string 型別鑑別子、所有 int 型別鑑別子,或完全不使用鑑別子。 下列範例示範如何混合和比對型別鑑別子設定:

[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

在上述範例中,BasePoint 型別沒有型別鑑別子,而 ThreeDimensionalPoint 型別有 int 型別鑑別子,而且 FourDimensionalPoint 有 string 型別鑑別子。

重要

若要讓多型序列化能夠運作,序列化值的型別應該屬於多型基底類型。 這包括在序列化根層級值時,使用基底類型作為泛型型別參數、作為序列化屬性的宣告型別,或作為列化集合中的集合元素。

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

自訂型別鑑別子名稱

型別鑑別子的預設屬性名稱為 $type。 若要自訂屬性名稱,請使用 JsonPolymorphicAttribute,如下列範例所示:

[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

在上述程式碼中,JsonPolymorphic 屬性會將 TypeDiscriminatorPropertyName 設定為 "$discriminator" 值。 設定型別鑑別子名稱後,下列範例會顯示序列化為 JSON 的 ThreeDimensionalPoint 型別:

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 }

提示

請避免使用與您的型別階層中的屬性衝突的 JsonPolymorphicAttribute.TypeDiscriminatorPropertyName。

處理未知的衍生型別

若要處理未知的衍生類型,您必須使用基礎類型的註解來選擇加入此類支援。 請考慮以下型別階層:

[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

由於設定未明確選擇支援 FourDimensionalPoint,嘗試將 FourDimensionalPoint 實例序列化為 BasePoint 時會導致執行時異常:

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

您可以使用 JsonUnknownDerivedTypeHandling 列舉來變更預設行為,其可指定如下:

[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

您可以使用 FallBackToNearestAncestor 設定來切換回最接近宣告衍生型別的合約,而不是切換回基底類型:

[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

使用如上述範例的設定時,ThreeDimensionalPoint 型別會序列化為 BasePoint:

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

不過,回退到最近的祖先,就可能出現「菱形」歧義。 請考慮下列型別階層作為範例:

[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

在此情況下,BasePointWithTimeSeries 型別可以序列化為 BasePoint 或 IPointWithTimeSeries,因為這兩者都是直接上階。 嘗試將 BasePointWithTimeSeries 的執行個體序列化為 IPoint 時,這種模稜兩可的情況會導致擲回 NotSupportedException。

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

從封閉階層推斷多態性

從 .NET 11 開始,System.Text.Json可以從 C# 15 封閉階層推導導出型別。 透過 JsonPolymorphicAttribute.InferClosedTypePolymorphism 在一個階層上啟用推論:

[JsonPolymorphic(InferClosedTypePolymorphism = true)]
public closed class Shape { }
public sealed class Circle : Shape { }
public sealed class Square : Shape { }

序列化器會註冊每個導出型別,並以其簡單的型別名稱作為字串判別器。 例如,Circle 酬載包含 "$type":"Circle"。

要對期權實例所處理的每個封閉階層進行推論,請設:JsonSerializerOptions.InferClosedTypePolymorphism

var options = new JsonSerializerOptions
{
    InferClosedTypePolymorphism = true
};

產生來源時,設為 JsonSourceGenerationOptionsAttribute.InferClosedTypePolymorphism:

[JsonSourceGenerationOptions(InferClosedTypePolymorphism = true)]
[JsonSerializable(typeof(Shape))]
internal partial class AppJsonContext : JsonSerializerContext;

使用生成上下文時,請在來源生成屬性上設定推理。 只啟用執行時 JsonSerializerOptions 屬性,無法新增源產生器未輸出的衍生型元資料。

類型層級屬性優先於全域設定。 在 JsonPolymorphicAttribute 上設定 InferClosedTypePolymorphism = false,即可讓某個階層不受全域設定影響。

明確的 JsonDerivedTypeAttribute 註冊會取代該階層的推斷。 它們不會擴充推斷出的清單。 將 InferClosedTypePolymorphism = true 套用至不是 closed 的型別時,使用以反射為基礎的序列化會擲回 InvalidOperationException,並產生來源產生錯誤。 具有重複簡單名稱的推論型別會產生判別子碰撞,且每個推論型別的可達性至少與封閉基相同。

配置開放的通用派生型別

從 .NET 11 開始,當序列化器能從序列化基型中解析出唯一的閉型別時,JsonDerivedTypeAttribute接受一個開放的通用衍生型別:

[JsonDerivedType(typeof(Derived<>), "derived")]
public class Base<T>;
public class Derived<T> : Base<T>;
<JsonDerivedType(GetType(Derived(Of )), "derived")>
Public Class Base(Of T)
End Class
Public Class Derived(Of T)
    Inherits Base(Of T)
End Class

對於 Base<int>,序列化器會註冊 Derived<int>。 相同解析度支援通用介面、重新排序型別參數、巢狀通用參數、陣列,以及將部分基底型別參數固定為具體型別的導出型別。

每個導出型態參數必須可由封閉基底型態推導,替換必須明確無歧義,且所得型別必須滿足其通用約束條件。 基於反射的序列化會對不受支援的特化拋出 InvalidOperationException。 原始碼產生器會回報 SYSLIB1229,而且產生出的階層結構在序列化器進行設定時仍會失敗。 抑制警告並不代表登記有效。

使用契約模型設定多型

對於屬性註解不切實際或無法做到的使用情況(例如大型領域模型、跨組件階層,或第三方相依性中的階層),請使用合約模型來設定多型。 合約模型是一組 API,可用來在型別階層中設定多型,方法是建立可為每個型別動態提供多型設定的自訂 DefaultJsonTypeInfoResolver 子類別,如下列範例所示:

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

其他多型序列化詳細資料

  • 多型序列化支援透過 JsonDerivedTypeAttribute 明確選擇加入的衍生型別。 未宣告型別會導致執行時例外。 您可以設定 JsonPolymorphicAttribute.UnknownDerivedTypeHandling 屬性來變更此行為。
  • 基底型別中的多型設定不會繼承衍生型別中指定的多型設定。 您必須獨立設定基底型別。
  • interface 和 class 型別都支援多型階層。
  • 使用型別鑑別子的多型僅支援使用物件、集合和字典型別之預設轉換器的型別階層。
  • 中繼資料型來源產生支援多型,但不支援快速路徑來源產生。

另請參閱