C# JSONで派生クラスのコレクションをシリアライズ・デシリアライズする方法(リフレクションで自動生成)

コンピュータ

JSON形式でシリアライズするということは、
オブジェクトを文字列として表現することを意味します。

文字列化できるということは、その内容をテキストファイルとして保存することで、
データを永続化することができます。

また、保存された文字列をデシリアライズすることで、
元のオブジェクトを復元することも可能です。

ここで一つ疑問が出てきます。

複数の派生クラスのオブジェクトを要素として持つコレクションを、
JSON形式にシリアライズすることはできるのでしょうか?

シリアライズは単にオブジェクトを文字列に変換する処理なので、
問題なく実現できそうに思えます。

しかし、デシリアライズの場合は事情が少し異なります。
文字列からオブジェクトを生成する必要があるため、
どの型のオブジェクトを生成すればよいのかという 型情報(派生クラスの情報) が必要になります。

そこで今回は、
その型情報をJSONの中に含めることで、
デシリアライズ時に正しい派生クラスのオブジェクトを生成できるようにする
サンプルコードを作成してみました。

using System.Text.Json;

namespace JsonSample01;

// ベースクラス
public abstract class BaseClass
{
    public abstract void Apply();
}

// 派生クラス
public class DerivedA : BaseClass
{
    public int Value { get; set; } = 0;

    public override void Apply()
    {
        Console.WriteLine($"DerivedA {Value}");
    }
}

public class DerivedB : BaseClass
{
    public string Name { get; set; } = "";

    public override void Apply()
    {
        Console.WriteLine($"DerivedB {Name}");
    }
}

// JSON保存用DTO
public class BaseDto
{
    public string Type { get; set; } = "";
    public string Json { get; set; } = "";
}


public class Program
{

	// シリアライズ
	public static string Serialize(List<BaseClass> list)
	{
		var dtoList = new List<BaseDto>();

		foreach (var item in list)
		{
			dtoList.Add(new BaseDto
			{
				Type = item.GetType().FullName!,
				Json = JsonSerializer.Serialize(item, item.GetType()),
			});
		}

		return JsonSerializer.Serialize(dtoList, new JsonSerializerOptions
		{
			WriteIndented = true
		});
	}
	// デシリアライズ(リフレクション)
	public static List<BaseClass> Deserialize(string json)
	{
		var dtoList = JsonSerializer.Deserialize<List<BaseDto>>(json);

		var result = new List<BaseClass>();

		foreach (var dto in dtoList!)
		{
			var type = Type.GetType(dto.Type);

			if (type == null)
				throw new Exception($"Unknown type: {dto.Type}");

			var obj = (BaseClass)JsonSerializer.Deserialize(dto.Json, type)!;

			result.Add(obj);
		}

		return result;
	}

	// エントリポイント
	static void Main()
	{
		var list = new List<BaseClass>
		{
			new DerivedA { Value = 10 },
			new DerivedB { Name = "test" }
		};

		string json = Serialize(list);

		Console.WriteLine(json);
/*
出力結果:
[
  {
    "Type": "JsonSample01.DerivedA",
    "Json": "{\u0022Value\u0022:10}"
  },
  {
    "Type": "JsonSample01.DerivedB",
    "Json": "{\u0022Name\u0022:\u0022test\u0022}"
  }
]

\u0022 ... "
*/

		var restored = Deserialize(json);

		foreach (var item in restored)
		{
			item.Apply();
		}
/*
出力結果:
DerivedA 10
DerivedB test
*/
	}
}

サンプルコードでは、JSONに保存する型情報として
名前空間を含めたクラス名を保存しています。

dto.Type = item.GetType().FullName;

例えば DerivedA というクラスが

namespace JsonSample01

の中に定義されている場合、JSONには次のような文字列が保存されます。

"Type": "JsonSample01.DerivedA"

クラス名だけを保存することもできますが、
別の名前空間に同じクラス名が存在する場合、
どの型を生成すればよいか判別できなくなる可能性があります。

そのため、型情報としては

「名前空間 + クラス名」

の形式で保存するのが安全です。

コメント