前提
自分でJsonを書き出し、そしてそれを読み込むような場面でしか本稿のプログラムは動きません。
クラス定義
以下のようなクラスがあるとします。
// 継承元の抽象クラス(インターフェイスでも可)
abstract class ConfigBase
{
public string Name { get; set; } = "";
}
// 抽象クラスやインターフェイスから派生したクラス1
class ConfigType1 : ConfigBase
{
public int Count { get; set; } = 0;
}
// 抽象クラスやインターフェイスから派生したクラス2
class ConfigType2 : ConfigBase
{
public string Message { get; set; } = "";
}
List<ConfigBase>
という型のリストを用意すればConfigType1
もConfigType2
も、両方リストに放り込むことができますね。
var configs = new List<ConfigBase>();
configs.Add(new ConfigType1 { Name = "hoge", Count = 10});
configs.Add(new ConfigType2 { Name = "fuga", Message="piyo"});
そのままSerialize/Deserializeすると
これをJsonとして読み書きしたいとき、Serialize/Deserializeは自分の理想通りには動いてくれません。
まずはSerializeから。
using System.Text.Json;
var jsonText = JsonSerializer.Serialize(configs);
こんな風になってほしいところですが…
[
{
"Name":"hoge",
"Count": 10
},
{
"Name":"fuga",
"Message":"piyo"
}
]
ConfigBaseが持つプロパティのみシリアライズされます。
[
{
"Name":"hoge"
},
{
"Name":"fuga"
}
]
Deserializeはというと…
using System.Text.Json;
var configs = JsonSerializer.Deserialize<List<ConfigBase>>(jsonText);
インターフェイスや抽象クラスのDeserializeはサポートされてませんというエラーになります。
System.NotSupportedException:
Deserialization of interface or abstract types is not supported.
そりゃそうだ。そのインターフェイスや抽象クラスがどんな型に派生しているかの知識を持っていないと型変換できないし、まったく同じプロパティをもつ別の派生型があったらどちらの型に変換したらいいか判別できません。
解決策
そのインターフェイスや抽象クラスがどんな型に派生しているかの知識を持っていないと型変換できないし、
💡じゃあ知識を与えればいいんじゃない?
まったく同じプロパティをもつ別の派生型があったらどちらの型に変換したらいいか判別できません。
💡じゃあ元がなんの型だったかメモしておけばいいんじゃない?
JsonDerivedType属性
そのまんま思いついた解決策をやってくれる機能があります。
継承元のクラスにJsonDerivedType
属性を追記します。
+ using System.Text.Json.Serialization;
+ [JsonDerivedType(typeof(ConfigType1), typeDiscriminator: nameof(ConfigType1))]
+ [JsonDerivedType(typeof(ConfigType2), typeDiscriminator: nameof(ConfigType2))]
// 継承元の抽象クラス(インターフェイスでも可)
abstract class ConfigBase
{
public string Name { get; set; } = "";
}
JsonDerivedType
属性でどんな型に派生しているかを教えてあげます。
typeDiscriminator
引数についてはSerializeの実行結果を見るとわかります。
Serialize
属性を付けた状態でSerializeを実行すると以下のようになります。
[
{
"$type": "ConfigType1",
"Count": 10,
"Name": "hoge"
},
{
"$type": "ConfigType2",
"Message": "piyo",
"Name": "fuga"
}
]
理想の形になっています。
しかし、想定していた出力のほかに$type
というキーが追加されています。
これはJsonにする前はどんな型だったのかというメタデータで、この時の値としてtypeDiscriminator
で指定した値が使用されます。ここでは型名を使用しましたが、それぞれの型を区別さえできれば値はなんでもいいです。
Deserialize
出力されたJsonに元がどんな型だったかが記録されたので、Deserializeもすんなり実行されます。
var configsResult = JsonSerializer.Deserialize<List<ConfigBase>>(jsonText);
foreach (var config in configsResult)
{
Console.WriteLine(config);
}
ConfigType1 { Name = hoge, Count = 10 }
ConfigType2 { Name = fuga, Message = piyo }
おわりに
無事、多相型リストをSerialize/Deserializeすることができました。
しかし、お分かりのとおり、自分で型情報を出力したJsonならDeserializeできますが、外部サービス等から取得してきたJsonテキストには無力ですので、また別の方法を試す必要があります。
参考にしたページ