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)
このシナリオでは、weatherForecastBase
オブジェクトが実際には WeatherForecastWithCity
オブジェクトであるため、City
プロパティはシリアル化されません。 この構成では、特にランタイムの種類が WeatherForecastWithCity
である場合に、WeatherForecastBase
のポリモーフィックなシリアル化が有効になります。
{
"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
Note
既定では、$type
識別子は $id
や $ref
などの他のメタデータ プロパティと共に、JSON オブジェクトの先頭に配置する必要があります。 外部 API から読み取る JSON データで、$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())
コントラクト モデルを使用してポリモーフィズムを構成する
属性注釈が実用的でない、または適切でないというユース ケース (大規模なドメイン モデル、アセンブリ間階層、サード パーティの依存関係の階層など) の場合、ポリモーフィズムを構成するには、コントラクト モデルを使用します。 コントラクト モデルは 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
の両方の型でサポートされています。 - 型判別子を使用するポリモーフィズムは、オブジェクト、コレクション、ディクショナリ型用の既定のコンバーターを使用する型階層でのみサポートされます。
- ポリモーフィズムは、メタデータ ベースのソース生成ではサポートされていますが、高速パス ソースの生成ではサポートされていません。
関連項目
.NET