子要素が動的に変わるJSONをC＃でマッピングするときはちょっと大変（System.Text.Json）

（こちらもNewtonsoftの方でなく、System.Text.Json

## 要素が動的に変わるJSONはC＃で扱いにくい
例

``` json
{
      "id": "1",
      "type": "A",
      "attributes": {
       (ここの定義が上のtypeにより変わる)
      }
}
```

`dynamic`型使ってもできるが、できればクラスを定義して使いたいとき
ポリモーフィックなJSONを扱うには、という感じで公式でも解説されてはいる。
https://docs.microsoft.com/ja-jp/dotnet/standard/serialization/system-text-json-converters-how-to?pivots=dotnet-6-0#support-polymorphic-deserialization

## コーディング指針
* 各attributesに共通する要素を持つ親クラスを作る（共通してなかったら空でも良い）
* Converter内でtypeによりシリアライズするクラスを変える

## マッピングする先のクラス定義

``` class.cs
// 基底クラス、共通する要素があるならここに入れとく、プロパティが空でもOK
public class BaseAttributes
{
}

public class A : BaseAttributes
{
  public int id { get; set; }
}

public class B : BaseAttributes
{
  public int price { get; set; }
}

// 他にもあれば
```

なお、基底クラスを`abstract`宣言しているとエラーになるので、注意（Newtonsoftの場合のコードと混同してやってた）

``` エラー内容
Unhandled exception. System.NotSupportedException: Deserialization of reference types without parameterless constructor is not supported. Type 'JsonConverterTutorial.BaseAttributes'
   at System.Text.Json.ThrowHelper.ThrowNotSupportedException_DeserializeCreateObjectDelegateIsNull(Type invalidType)

```

## コンバーター実装
https://docs.microsoft.com/ja-jp/dotnet/standard/serialization/system-text-json-converters-how-to?pivots=dotnet-6-0#converter-samples-for-common-scenarios
まずは公式からのサンプル

``` converter.cs
public class ObjectToInferredTypesConverter : JsonConverter<object>
{
    public override object Read(
        ref Utf8JsonReader reader,
        Type typeToConvert,
        JsonSerializerOptions options) => reader.TokenType switch
        {
            JsonTokenType.True => true,
            JsonTokenType.False => false,
            JsonTokenType.Number when reader.TryGetInt64(out long l) => l,
            JsonTokenType.Number => reader.GetDouble(),
            JsonTokenType.String when reader.TryGetDateTime(out DateTime datetime) => datetime,
            JsonTokenType.String => reader.GetString()!,
            _ => JsonDocument.ParseValue(ref reader).RootElement.Clone()
        };

    public override void Write(
        Utf8JsonWriter writer,
        object objectToWrite,
        JsonSerializerOptions options) =>
        JsonSerializer.Serialize(writer, objectToWrite, objectToWrite.GetType(), options);
}
```

もひとつ

``` converter.cs
using System.Text.Json;
using System.Text.Json.Serialization;

namespace SystemTextJsonSamples
{
    public class PersonConverterWithTypeDiscriminator : JsonConverter<Person>
    {

        public override Person Read(
            ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            if (reader.TokenType != JsonTokenType.StartObject)
            {
                throw new JsonException();
            }

            reader.Read();
            if (reader.TokenType != JsonTokenType.PropertyName)
            {
                throw new JsonException();
            }

            string? propertyName = reader.GetString();
            if (propertyName != "TypeDiscriminator")
            {
                throw new JsonException();
            }

            reader.Read();
            if (reader.TokenType != JsonTokenType.Number)
            {
                throw new JsonException();
            }

            TypeDiscriminator typeDiscriminator = (TypeDiscriminator)reader.GetInt32();
            Person person = typeDiscriminator switch
            {
                TypeDiscriminator.Customer => new Customer(),
                TypeDiscriminator.Employee => new Employee(),
                _ => throw new JsonException()
            };

            while (reader.Read())
            {
                if (reader.TokenType == JsonTokenType.EndObject)
                {
                    return person;
                }

                if (reader.TokenType == JsonTokenType.PropertyName)
                {
                    propertyName = reader.GetString();
                    reader.Read();
                    switch (propertyName)
                    {
                        case "CreditLimit":
                            decimal creditLimit = reader.GetDecimal();
                            ((Customer)person).CreditLimit = creditLimit;
                            break;
                        case "OfficeNumber":
                            string? officeNumber = reader.GetString();
                            ((Employee)person).OfficeNumber = officeNumber;
                            break;
                        case "Name":
                            string? name = reader.GetString();
                            person.Name = name;
                            break;
                    }
                }
            }

            throw new JsonException();
        }
        // read以外は省略

}
```

後者のサンプルを見ると、どうやら`reader`でJSONの要素を順次読み込みしていっているみたいだ。
となると、今回の例だと`type`の値により`attributes`を割り当てるクラスを変えれば良さそうだ。
ただ、ここだと`attributes`の部分もオブジェクトになっているので、もとの文字列がほしい。

### readerから元のJSON string取得

``` cs
using (var jsonDoc = JsonDocument.ParseValue(ref reader))
{
    valueString = jsonDoc.RootElement.GetRawText();
}
```

`reader`からもとの文字列を取得してくる。

`tokenType`でプロパティなのかなんなのか（いろいろあるみたい）判定できる。
実際に書いたコードを一部抜粋して

``` cs
  if (reader.TokenType == JsonTokenType.PropertyName)
  {
      string propertyName = reader.GetString()!;
      reader.Read();
      var valueString = "";
      using (var jsonDoc = JsonDocument.ParseValue(ref reader))
      {
          valueString = jsonDoc.RootElement.GetRawText();
      }
      switch (propertyName)
      {
          case "id":
              iv.id = reader.GetString();
              break;
          case "type":
              iv.type = reader.GetString();
              attrType = iv.type;
              break;
          case "attributes":
              switch (attrType)
              {
                  case "A":
                      iv.attributes = JsonSerializer.Deserialize<A>(valueString);
                      break;
                  case "B":
                      iv.attributes = JsonSerializer.Deserialize<B>(valueString);
                      break;
                  case "C":
                      iv.attributes = JsonSerializer.Deserialize<C>(valueString);
                      break;
              }
              break;
      }
  }
```

こんな感じで`type`の値によりデシリアライズする先のクラスを動的に変える。

動かすときのコードは

``` cs
var obj = JsonSerializer.Deserialize<BaseAttributes>(json, new JsonSerializerOptions() {Converters = { new AttributesConverter() }});
```

で、OK

ただ、実際に`attributes`にアクセスするときはちゃんとキャストしよう。

``` cs
Console.WriteLine(((A)obj3.attributes).aprop);
```

### 大変だった
どう調べていいかわからずまず右往左往、公式にあったが今回のケースにはまるわけじゃないので、色々と検証必要だった。