C#でJsonConverterを簡単に書けるようにしてみた

# はじめに
Newtonsoft.Json で、jsonからそのままでは無く構造を変えたクラスに変換するにはカスタムコンバータを作成する必要があります。

例えば、jsonが以下の時。
``` Json
{
  "Name": "MyName",
  "Address": {
    "Prefecture" : "東京都",
    "City" : "港区",
    "Town" : "六本木",
    "HouseNumber" : "6-10-1"
  } 
}
```
これを読み込んで以下の動きをするクラスを生成する場合、
``` C#
    public class User
    {
        public string Name => "MyName";
        public Address Address => new Address();
    }
    public class Address
    {
        public override string ToString() => "東京都 港区 六本木 6-10-1";
    }
```

Jsonコンバータを作る必要があり、作ってもそのクラス専用となるので、毎回作り直す必要が有ります。
とても面倒くさい(※1)ので、どこでもこれ一本で汎用で使えるカスタムコンバータを作ってみました。

# 環境
Windows10 Pro 22H2
Visual Studio 2022 17.3.0
Newtonsoft.Json 13.0.2

# カスタムコンバータ
これをそのまま使います。

``` C#
    using Newtonsoft.Json;

    /// <summary>
    /// Newtonsoft.JsonのJsonConverterを簡単に実装する
    /// </summary>
    /// <typeparam name="T">Json変換対象クラス</typeparam>
    /// <typeparam name="ModelT">実際のJson構造を表すモデルクラス。変換対象クラスが内部保持する</typeparam>
    public class JsonModelConverter<T, ModelT> : JsonConverter
        where T : IJsonModel
    {
        public override bool CanConvert(Type objectType) => objectType == typeof(T);

        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            var model = serializer.Deserialize(reader, typeof(ModelT));  // JsonをUserModelに変換
            return Activator.CreateInstance(typeof(T), model);  // new User(model); 相当
        }

        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            var o = value as IJsonModel;
            serializer.Serialize(writer, o.GetModel()); // UserModelをjsonに変換
        }
    }

    /// <summary>
    /// 変換対象クラスで実装する
    /// </summary>
    public interface IJsonModel
    {
        /// <summary>
        /// Json変換対象クラスが内部保持するモデルオブジェクトを取得する
        /// </summary>
        /// <returns></returns>
        object GetModel();
    }
```

# モデルクラス
jsonをそのままの構成でマッピングするモデルクラスを作成します。

既にjsonのテキストサンプルが有るなら、Visual Studioで簡単に作成することが出来ます。
自分で書いてもいいですが、既にjsonのテキストがあるならそこから自動生成することが可能です。

手順：
1. Jsonのテキストを選択してコピー
2. C#のソースファイルを開いて、メニューから[編集]-[形式を選択して貼り付け]-[JSONをクラスとして貼り付ける] にてクラスを生成する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120192/38b7a463-fa91-9f75-d28c-3db0bf023dc7.png)
3. 生成されたクラスの名前などを整える。後で作成する変換対象クラスと区別する為にクラス名にModelとか付けておくことをお勧め。
``` C#
    public class UserModel
    {
        public string Name { get; set; }
        public AddressModel Address { get; set; }
    }
    public class AddressModel
    {
        public string Prefecture { get; set; }  // 都道府県
        public string City { get; set; }  // 市区町村
        public string Town { get; set; }  // 町域名
        public string HouseNumber { get; set; }  // 番地
    }
```

# 変換対象クラス
先ほどのモデルクラスをフィールドに持った最終目的のクラスを作成します。
``` C#
    [JsonConverter(typeof(JsonModelConverter<User,UserModel>))]
    public class User : IJsonModel
    {
        UserModel _model;
        public User(UserModel model) => _model = model;

        public string Name => _model.Name;
        public Address Address => new Address(_model.Address);

        public object GetModel() => _model;
    }
    public class Address
    {
        AddressModel _model;
        public Address(AddressModel model) => _model = model;

        public override string ToString() => $"{_model.Prefecture} {_model.City} {_model.Town} {_model.HouseNumber}";
    }
```
Userクラスは、JsonModelConverterを指定し、IJsonModelインターフェースを継承して内部保持しているモデルクラスのインスタンスを返します。

# 実行
Json -> Userクラス
``` C#
    var user = JsonConvert.DeserializeObject<User>(jsonText);
    ....
    var address = user.Address.ToString();  // "東京都 港区 六本木 6-10-1"
```

Userクラス -> Json
``` C#
    User user = ...
    var jsonText = JsonConvert.SerializeObject(user);
```

# 課題
Modelクラスとか、IJsonModelインターフェースのメソッドとかのアクセスビリティがpublicの必要が有ります。
実際にはinternalとかprivateにしたいところですが、それは今後の課題です。

# Special Thanks
[ChatGPT](https://chat.openai.com/chat) : サンプルの作成を手伝って頂きました。

※1
ChatGPTさんに「JsonConverterの実装サンプルコードを書いてください」とお願いしてみて下さい。

ちなみに私が頂いたコードは以下です。

``` C#
using Newtonsoft.Json;

public class CustomType
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class CustomTypeConverter : JsonConverter
{
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        // CustomTypeをJSONに変換する処理
        CustomType customType = (CustomType)value;
        writer.WriteStartObject();
        writer.WritePropertyName("id");
        writer.WriteValue(customType.Id);
        writer.WritePropertyName("name");
        writer.WriteValue(customType.Name);
        writer.WriteEndObject();
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        // JSONをCustomTypeに変換する処理
        CustomType customType = new CustomType();
        while (reader.Read())
        {
            if (reader.TokenType == JsonToken.PropertyName)
            {
                string propertyName = (string)reader.Value;
                reader.Read(); // プロパティの値を読み込む
                if (propertyName == "id")
                {
                    customType.Id = (int)reader.Value;
                }
                else if (propertyName == "name")
                {
                    customType.Name = (string)reader.Value;
                }
            }
            else if (reader.TokenType == JsonToken.EndObject)
            {
                break;
            }
        }
        return customType;
    }

    public override bool CanConvert(Type objectType)
    {
        // このコンバーターが適用される型を指定する
        return objectType == typeof(CustomType);
    }
}

// シリアライズする際にJsonConverterを使用する
CustomType customType = new CustomType { Id = 1, Name = "foo" };
string json = JsonConvert.SerializeObject(customType, new CustomTypeConverter());

// デシリアライズする際にJsonConverterを使用する
CustomType customType = JsonConvert.DeserializeObject<CustomType>(json, new CustomTypeConverter());
```
こんな感じで毎回書くのは地獄なので今回のモデルコンバータを作成しました。