System.Text.Json での派生クラスのシリアライズ

## このドキュメントの内容

System.Text.Json.JsonSerializer クラスでの派生クラスのシリアライズについて簡単に説明します。
詳しくは、マイクロソフトの [.NET ガイド] (https://docs.microsoft.com/ja-jp/dotnet/standard/serialization/system-text-json-how-to#serialize-properties-of-derived-classes) の派生クラスのプロパティのシリアル化の項で説明されています。

## サンプルクラス

ローカルファイルまたはデータベースをデータソースとするアプリケーションの動作設定を、次のようなコンフィグクラスで表したとします。FileConfig クラスと DatabaseConfig クラスは DataSourceConfig クラスから派生しています。

```c#
public class ApplicationConfig
{
    public DataSourceConfig DataSource { get; set; }
}

public class DataSourceConfig
{
    public string Name { get; set; }
}

public class FileConfig : DataSourceConfig
{
    public string FilePath { get; set; }
}

public class DatabaseConfig : DataSourceConfig
{
    public string ConnectionString { get; set; }
}
```

## シリアライズ対象のインスタンス自身が派生型である場合

### 単純にシリアライズしたときの結果

```c#
private string SerializeDataSourceConfig()
{
    // 基底型で宣言
    DataSourceConfig config = new FileConfig()
    {
        Name = "file1",
        FilePath = @"d:\test.csv"
    };

    return JsonSerializer.Serialize(config, GetOptions());
}

private string SerializeFileConfig()
{
    // 派生型で宣言
    FileConfig config = new FileConfig()
    {
        Name = "file1",
        FilePath = @"d:\test.csv"
    };

    return JsonSerializer.Serialize(config, GetOptions());
}

private JsonSerializerOptions GetOptions()
{
    return new JsonSerializerOptions()
    {
        WriteIndented = true
    };
}
```

シリアライザに渡された型の型情報から出力対象のプロパティが決定されます。基底型である DataSourceConfig クラスが渡された場合、派生型で定義されているプロパティは出力対象になりません。

```console:SerializeDataSourceConfigメソッドの戻り値
{
  "Name": "file1"
}
```
```console:SerializeFileConfigメソッドの戻り値
{
  "FilePath": "d:\\test.csv",
  "Name": "file1"
}
```

### 派生型で定義されているプロパティを出力対象にするには

Serialize メソッドの引数にシリアライズ対象インスタンスの型を渡すか、Serialize メソッドの型情報に object を指定します。

```c#
private string SerializeDataSourceInheritConfig1()
{
    // 基底型で宣言
    DataSourceConfig config = new FileConfig()
    {
        Name = "file1",
        FilePath = @"d:\test.csv"
    };

    // 引数でインスタンスの型を渡す
    return JsonSerializer.Serialize(config, config.GetType(), GetOptions());
}

private string SerializeDataSourceInheritConfig2()
{
    // 基底型で宣言
    DataSourceConfig config = new FileConfig()
    {
        Name = "file1",
        FilePath = @"d:\test.csv"
    };

    // ジェネリックパラメーターで object を指定する
    return JsonSerializer.Serialize<object>(config, GetOptions());
}
```

どちらも出力結果は同じです。FileCondig クラスで定義されているプロパティの値も出力されます。

```console:SerializeDataSourceInheritConfig1メソッドの戻り値
{
  "FilePath": "d:\\test.csv",
  "Name": "file1"
}
```
```console:SerializeDataSourceInheritConfig2メソッドの戻り値
{
  "FilePath": "d:\\test.csv",
  "Name": "file1"
}
```

## シリアライズ対象インスタンスのプロパティに、その派生型のインスタンスが格納されている場合

### 単純にシリアライズしたときの結果

```c#
private string SerializeApplicationConfig()
{
    ApplicationConfig config = new ApplicationConfig()
    {
        DataSource = new FileConfig()
        {
            Name = "file1",
            FilePath = @"d:\test.csv"
        }
    };

    return JsonSerializer.Serialize(config, GetOptions());
}
```

ApplicationConfig.DataSource の型は DataSourceConfig クラスであるため、FileConfig で定義されているプロパティは出力対象になりません。

```console:SerializeApplicationConfigメソッドの戻り値
{
  "DataSource": {
    "Name": "file1"
  }
}
```

### 派生型で定義されているプロパティを出力対象にするには

古典的な方法ですが、シリアライズ目的の object 型プロパティを定義します。本来のプロパティはシリアライズ対象外になるように属性でマークします。

前述の ApplicationConfig クラスを次のように変更します。

* DataSource プロパティに JsonIgnore 属性を付与します。
* DataSource プロパティの値も読み書きを行う object 型の DataSourceObject プロパティを定義します。
  * JsonPropertyName 属性を付与し、"DataSource" という名前でシリアライズされるようにしています。
  * Browsable 属性と EditorBrowsable 属性を付与し、表に現れにくくしています。
   
```c#
// using System.ComponentModel;
// using System.Text.Json.Serialization;

public class ApplicationConfig
{
    [JsonIgnore]
    public DataSourceConfig DataSource { get; set; }

    [JsonPropertyName("DataSource")]
    [Browsable(false)]
    [EditorBrowsable( EditorBrowsableState.Never)]
    public object DataSourceObject
    {
        get { return DataSource; }
        set { DataSource = (DataSourceConfig)value; }
    }
}
```

FileConfig クラスで定義されているプロパティもシリアライズ対象になります。DataSourceObject プロパティの値がシリアライズされる際、JsonSerializer.Serialize メソッドに object 型が渡され、プロパティの型ではなくインスタンスの型が使用されるようになるためです。

```console:SerializeApplicationConfigメソッドの戻り値
{
  "DataSource": {
    "FilePath": "d:\\test.csv",
    "Name": "file1"
  }
}
```

## まとめ

アプリケーションの動作設定をファイルから読み込むような目的であれば、この方法で問題はなさそうです。