Bagikan melalui


Cara menserialisasi properti kelas turunan dengan System.Text.Json

Dalam artikel ini, Anda mempelajari cara membuat serialisasi properti kelas turunan dengan System.Text.Json namespace.

Menserialisasikan properti kelas turunan

Dimulai dengan .NET 7, System.Text.Json mendukung serialisasi dan deserialisasi hierarki jenis polimorfik dengan anotasi atribut.

Atribut Deskripsi
JsonDerivedTypeAttribute Ketika ditempatkan pada deklarasi jenis, menunjukkan bahwa subjenis yang ditentukan harus dipilih ke dalam serialisasi polimorfik. Ini juga mengekspos kemampuan untuk menentukan jenis diskriminator.
JsonPolymorphicAttribute Ketika ditempatkan pada deklarasi jenis, menunjukkan bahwa jenis harus diserialisasikan secara polimorfik. Ini juga mengekspos berbagai opsi untuk mengonfigurasi serialisasi polimorfik dan deserialisasi untuk jenis tersebut.

Misalnya, Anda memiliki WeatherForecastBase kelas dan kelas turunan 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

Dan misalkan argumen jenis Serialize<TValue> metode pada waktu kompilasi adalah WeatherForecastBase:

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

Dalam skenario ini, properti diserialisasikan City karena weatherForecastBase objek sebenarnya adalah WeatherForecastWithCity objek. Konfigurasi ini memungkinkan serialisasi polimorfik untuk WeatherForecastBase, khususnya ketika jenis runtime adalah WeatherForecastWithCity:

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

Meskipun round-tripping payload seperti WeatherForecastBase yang didukung, payload tidak akan terwujud sebagai jenis run-time .WeatherForecastWithCity Sebaliknya, itu akan terwujud sebagai jenis run-time dari 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

Bagian berikut menjelaskan cara menambahkan metadata untuk mengaktifkan round-tripping dari jenis turunan.

Diskriminator jenis polimorfik

Untuk mengaktifkan deserialisasi polimorfik, Anda harus menentukan jenis diskriminator untuk kelas turunan:

[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

Dengan metadata tambahan, khususnya, diskriminator jenis, serializer dapat menserialisasikan dan mendeserialisasi payload sebagai WeatherForecastWithCity jenis dari jenis WeatherForecastBasedasarnya . Serialisasi memancarkan JSON bersama dengan metadata diskriminator jenis:

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

Dengan jenis diskriminator, serializer dapat mendeserialisasi payload secara polimorfik sebagai 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

Catatan

Secara default, $type diskriminator harus ditempatkan di awal objek JSON, dikelompokkan bersama dengan properti metadata lainnya seperti $id dan $ref. Jika Anda membaca data dari API eksternal yang menempatkan $type diskriminator di tengah objek JSON, atur JsonSerializerOptions.AllowOutOfOrderMetadataProperties ke true:

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

Berhati-hatilah saat Anda mengaktifkan bendera ini, karena dapat mengakibatkan kegagalan over-buffering (dan kegagalan di luar memori) saat melakukan deserialisasi streaming objek JSON yang sangat besar.

Mencampur dan mencocokkan format diskriminator jenis

Pengidentifikasi diskriminator jenis valid dalam bentuk string atau int , sehingga berikut ini valid:

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

Meskipun API mendukung konfigurasi diskriminator jenis pencampuran dan pencocokan, api tidak disarankan. Rekomendasi umumnya adalah menggunakan semua string jenis diskriminator, semua int jenis diskriminator, atau tidak ada diskriminator sama sekali. Contoh berikut menunjukkan cara mencampur dan mencocokkan konfigurasi diskriminator jenis:

[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

Dalam contoh sebelumnya, BasePoint jenis tidak memiliki diskriminator jenis, sementara ThreeDimensionalPoint jenis memiliki int diskriminator jenis, dan FourDimensionalPoint memiliki string diskriminator jenis.

Penting

Agar serialisasi polimorfik berfungsi, jenis nilai berseri harus dari jenis dasar polimorfik. Ini termasuk menggunakan jenis dasar sebagai parameter jenis generik saat menserialisasikan nilai tingkat akar, sebagai jenis properti serial yang dideklarasikan, atau sebagai elemen koleksi dalam koleksi yang diserialisasikan.

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

Mengkustomisasi nama diskriminator jenis

Nama properti default untuk jenis diskriminator adalah $type. Untuk mengkustomisasi nama properti, gunakan seperti yang JsonPolymorphicAttribute diperlihatkan dalam contoh berikut:

[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

Dalam kode sebelumnya, JsonPolymorphic atribut mengonfigurasi TypeDiscriminatorPropertyName ke "$discriminator" nilai . Dengan nama diskriminator jenis yang dikonfigurasi, contoh berikut menunjukkan jenis yang diserialisasikan ThreeDimensionalPoint sebagai 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 }

Tip

Hindari menggunakan JsonPolymorphicAttribute.TypeDiscriminatorPropertyName yang berkonflik dengan properti dalam hierarki jenis Anda.

Menangani jenis turunan yang tidak diketahui

Untuk menangani jenis turunan yang tidak diketahui, Anda harus ikut serta dalam dukungan tersebut menggunakan anotasi pada jenis dasar. Pertimbangkan hierarki jenis berikut:

[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

Karena konfigurasi tidak secara eksplisit memilih dukungan untuk FourDimensionalPoint, mencoba membuat serialisasi instans sebagaimana FourDimensionalPoint BasePoint akan mengakibatkan pengecualian run-time:

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

Anda dapat mengubah perilaku default dengan menggunakan JsonUnknownDerivedTypeHandling enum, yang dapat ditentukan sebagai berikut:

[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

Alih-alih kembali ke jenis dasar, Anda dapat menggunakan FallBackToNearestAncestor pengaturan untuk kembali ke kontrak jenis turunan terdekat yang dinyatakan:

[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

Dengan konfigurasi seperti contoh sebelumnya, ThreeDimensionalPoint jenis akan diserialisasikan sebagai BasePoint:

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

Namun, jatuh kembali ke leluhur terdekat mengakui kemungkinan ambiguitas "berlian". Pertimbangkan hierarki jenis berikut sebagai contoh:

[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

Dalam hal ini, jenisnya BasePointWithTimeSeries dapat diserialisasikan sebagai atau BasePoint IPointWithTimeSeries karena keduanya adalah leluhur langsung. Ambiguitas ini akan menyebabkan NotSupportedException dilemparkan ketika mencoba menserialisasikan instans sebagai BasePointWithTimeSeries IPoint.

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

Mengonfigurasi polimorfisme dengan model kontrak

Untuk kasus penggunaan di mana anotasi atribut tidak praktis atau tidak mungkin (seperti model domain besar, hierarki perakitan silang, atau hierarki dalam dependensi pihak ketiga), untuk mengonfigurasi polimorfisme menggunakan model kontrak. Model kontrak adalah sekumpulan API yang dapat digunakan untuk mengonfigurasi polimorfisme dalam hierarki jenis dengan membuat subkelas kustom DefaultJsonTypeInfoResolver yang secara dinamis menyediakan konfigurasi polimorfik per jenis, seperti yang ditunjukkan dalam contoh berikut:

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

Detail serialisasi polimorfik tambahan

  • Serialisasi polimorfik mendukung jenis turunan yang telah secara eksplisit dipilih melalui JsonDerivedTypeAttribute. Jenis yang tidak dinyatakan akan menghasilkan pengecualian run-time. Perilaku dapat diubah dengan mengonfigurasi JsonPolymorphicAttribute.UnknownDerivedTypeHandling properti.
  • Konfigurasi polimorfik yang ditentukan dalam jenis turunan tidak diwariskan oleh konfigurasi polimorfik dalam jenis dasar. Jenis dasar harus dikonfigurasi secara independen.
  • Hierarki polimorfik didukung untuk jenis interface dan class .
  • Polimorfisme menggunakan diskriminator jenis hanya didukung untuk hierarki jenis yang menggunakan pengonversi default untuk objek, koleksi, dan jenis kamus.
  • Polimorfisme didukung dalam pembuatan sumber berbasis metadata, tetapi bukan pembuatan sumber jalur cepat.

Lihat juga