前提として
NewtonsoftのJson.NETを使ってJSONへ変換するとき、インターフェースを実装しているクラスは名前空間を含めた型情報を含めないといけないですよね…
例えば、以下のクラス構成の場合
namespace JsonSerializer.Signals
{
// 信号のインターフェース
public interface ISignal
{
string Color { get; set; }
}
// 赤信号
public class RedSignal : ISignal
{
public string Color { get; set; }
public RedSignal()
{
this.Color = "Red";
}
}
// 緑信号
public class GreenSignal : ISignal
{
public string Color { get; set; }
public GreenSignal()
{
this.Color = "Green";
}
}
// 黄色信号
public class YellowSignal : ISignal
{
public string Color { get; set; }
public YellowSignal()
{
this.Color = "Yellow";
}
}
}
以下のようにJsonSerializerSettingsにTypeNameHandling.Autoを設定して型情報を一緒に出力しないとデシリアライズが上手くいきません。
using JsonSerializer.Signals;
using Newtonsoft.Json;
namespace JsonSerializer
{
public class Program
{
public static void Main(string[] args)
{
// JSONへ変換するオブジェクトを生成
var signals = new List<ISignal>
{
new RedSignal(),
new GreenSignal(),
new YellowSignal()
};
var settings = new JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.Auto, // 型情報を含める
Formatting = Formatting.Indented, // インデントを揃える
};
// シリアライズ
var json = JsonConvert.SerializeObject(signals, settings);
Console.WriteLine("=== シリアライズ結果 ==");
Console.WriteLine(json);
// デシリアライズ
var deserialized = JsonConvert.DeserializeObject<List<ISignal>>(json, settings);
Console.WriteLine("\n=== デシリアライズ結果 ===");
Console.WriteLine($"要素数: {deserialized?.Count ?? 0}");
}
}
}
出力結果
→ "$type"が一緒に出力されていることに注目です☝️
=== シリアライズ結果 ==
[
{
"$type": "JsonSerializer.Signals.RedSignal, JsonSerializer",
"Color": "Red"
},
{
"$type": "JsonSerializer.Signals.GreenSignal, JsonSerializer",
"Color": "Green"
},
{
"$type": "JsonSerializer.Signals.YellowSignal, JsonSerializer",
"Color": "Yellow"
}
]
=== デシリアライズ結果 ===
要素数: 3
型情報がない場合、JsonSerializationExceptionが発生します。
デシリアライズするときにISignalの実装クラスのうち、どのクラスの型に戻したらいいかわからないからですね。
Unhandled exception. Newtonsoft.Json.JsonSerializationException:
Could not create an instance of typeJsonSerializer.Signals.ISignal.
Type is an interface or abstract class and cannot be instantiated. Path 'Signals[0].Color', line 4, position 14
実現したいこと
JSONに変換した内容をファイルに保存して管理するようにしていたのですが、名前空間を含めた情報はセキュリティや情報漏洩の観点からユーザーの見えるところにおいておくべきではありません。
つまり、名前空間の情報なしでインターフェースのデシリアライズを実現する必要があるわけです🤔
解決方法
JsonConverterを継承するカスタムコンバータを実装することで解決です!
以下のような動きを実装していきます。
| タイミング | 変換の流れ |
|---|---|
| シリアライズ | C#のType → 任意の文字列 |
| デシリアライズ | シリアライズ時に指定した文字列 → C#のType |
using System.Reflection;
using JsonSerializer.Signals;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace JsonSerializer
{
public class SignalTypeConverter : JsonConverter<ISignal>
{
// JSON変換時に型名を保持しておくプロパティ名
private static readonly string TYPE_PROPERTY = "type";
private Dictionary<string, Type> AllowedTypes = new Dictionary<string, Type>();
public SignalTypeConverter()
{
// ISignalを実装しているクラス一覧をリフレクションで取得
var concreteTypes = Assembly.GetEntryAssembly()
.GetTypes()
.Where(t => typeof(ISignal).IsAssignableFrom(t) && t.IsClass && !t.IsAbstract)
.ToList();
// Dictionary化
foreach (var type in concreteTypes)
{
this.AllowedTypes.Add(type.Name, type);
}
}
// シリアライズ時
public override void WriteJson(JsonWriter writer, ISignal? value, Newtonsoft.Json.JsonSerializer serializer)
{
if (value is null) return;
var obj = new JObject();
var typeKey = AllowedTypes.FirstOrDefault(at => at.Value == value.GetType()).Key;
obj[TYPE_PROPERTY] = typeKey; // JSONのtypeプロパティにキーを格納
// serializerをそのまま渡すとSignalTypeConverterの循環参照になってしまう
// このコンバーターを除外した設定で再シリアライズ
var tempSerializer = new Newtonsoft.Json.JsonSerializer
{
ContractResolver = serializer.ContractResolver,
Formatting = serializer.Formatting,
};
// プロパティをシリアライズ
var properties = JObject.FromObject(value, tempSerializer);
foreach (var prop in properties.Properties())
{
obj[prop.Name] = prop.Value;
}
obj.WriteTo(writer);
}
// デシリアライズ時
public override ISignal? ReadJson(JsonReader reader, Type objectType, ISignal? existingValue, bool hasExistingValue, Newtonsoft.Json.JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
{
return null;
}
var obj = JObject.Load(reader);
var typeKey = obj[TYPE_PROPERTY]?.ToString(); // シリアライズ時に指定した文字列を取得
var targetType = AllowedTypes[typeKey]; // C#で扱うTypeへ変換
obj.Remove(TYPE_PROPERTY); // 文字列で設定していたプロパティは削除
// シリアライズと同様に新しいシリアライザーを使用
var tempSerializer = new Newtonsoft.Json.JsonSerializer
{
ContractResolver = serializer.ContractResolver
};
return (ISignal)obj.ToObject(targetType, tempSerializer);
}
}
}
これでJSON変換時に"type"というプロパティに型変換用のキーを登録できます!serializerをそのまま渡すと循環参照の例外が発生することには注意が必要ですね。
後はProgram.csのシリアライズを実行している箇所で作成したカスタムコンバータを設定に付け加えるだけです👍
var settings = new JsonSerializerSettings
{
- TypeNameHandling = TypeNameHandling.Auto, // 型情報を含める
+ Converters = new List<JsonConverter> { new SignalTypeConverter() }, // カスタムコンバータを追加
Formatting = Formatting.Indented, // インデントを揃える
};
// シリアライズ
var json = JsonConvert.SerializeObject(signals, settings);
Console.WriteLine("=== シリアライズ結果 ==");
Console.WriteLine(json);
// デシリアライズ
var deserialized = JsonConvert.DeserializeObject<List<ISignal>>(json, settings);
Console.WriteLine("\n=== デシリアライズ結果 ===");
Console.WriteLine($"要素数: {deserialized?.Count ?? 0}");
修正後の出力内容
=== シリアライズ結果 ==
[
{
"type": "RedSignal",
"Color": "Red"
},
{
"type": "GreenSignal",
"Color": "Green"
},
{
"type": "YellowSignal",
"Color": "Yellow"
}
]
=== デシリアライズ結果 ===
要素数: 3
独自設定の"type"が出力され、名前空間の情報は排除できました🎉
クラス名をそのまま使うのではなく、enumを使うのでも良さそうですね。