C# (.NET) の System.Text.Json を使って kintone のレコードをパースする

# C# って聞いて日和ってるやついる？

**いねえよなぁ!!?**

…どうもこんにちゎ……(震)、株式会社ウィルビジョンの住田です。

今年も弊社は kintone hack NIGHT に出場させていただいたのですが、今年は既に長々と [コラム](https://0cf950b5.viewer.kintoneapp.com/public/670a8a0273b8b53870d14e0a470b44ab215dca5623ae1e8746884bc89da1e3c8#/) を書かせていただいたので、アドベントカレンダーにはちょっと違った記事を書こうと思いました。

実はわたくし、こう見えて C# 大好きなんです。普段はバリバリ TypeScript とか JavaScript とか `#!/bin/bash` とか書いていますが C# が大好きです。

**大好きです。**

というわけで C# で kintone を使う記事を書いてみようという企画です。

kintone 界隈ではあまり C# って使われていない印象なので、もしこれを機に興味を持った方がいらっしゃったらぜひ C# を書いてみてください。

ただ、おもに社内のコードの一部分を抜粋したものを貼り付けていますので、そのままコピーしても動かないと思います。そこはぜひ動くように頑張ってみてください (投げやり)

それではゆっくりしていってね！


# `System.Text.Json`

.NET Core 系列になって、これまで痒い所に手が届かなかった機能がどんどん追加されてきました。その中でも特に `System.Text.Json` は多くの人に待ち望まれた機能ではないでしょうか。

実行環境に備え付けの基本機能だけで JSON がシリアライズ・デシリアライズできるという優れもの。今までありそうでなかったやつ。

JavaScript ほど簡単に…とはいきませんが、以下のようなコードで JSON をシリアライズ・デシリアライズできます。[^1]

```c#:このコードはC#10でちゃんと動きます
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Encodings.Web;

// 文字列を Unicode エスケープしない設定
var option = new JsonSerializerOptions { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, };

// シリアライズ
var obj = new SampleClass { Name = "ひげおじちゃん", Age = 10024, Gender = Gender.Male, };
var json = JsonSerializer.Serialize<SampleClass>(obj, option);
Console.WriteLine(json);
// > {"name":"ひげおじちゃん","gender":"Male"}

// デシリアライズ
var text = "{\"name\":\"シン・ひげおじちゃん\",\"Age\":10000,\"gender\":\"Female\"}";
var deObj = JsonSerializer.Deserialize<SampleClass>(text, option);
if (deObj is not null) {
	Console.WriteLine(deObj.Name);
	// > ひげおじちゃん

	Console.WriteLine(deObj.Gender);
	// > Male
	
	Console.WriteLine(deObj.Age);
	// > 0 (JsonIgnore を指定しているので初期値)
} else {
	Console.WriteLine("deObj is null.");
}

// ↓使用クラス

class SampleClass {
	[JsonPropertyName("name")] // JSON 上でプロパティ名は変更できる
	public string? Name { get; init; }

	[JsonIgnore] // JSON には出力しない
	public int Age { get; init; }

	[JsonConverter(typeof(JsonStringEnumConverter))] // C# の enum は int だけど JSON 上で文字列にもできる
	[JsonPropertyName("gender")]
	public Gender Gender { get; init; }
}

enum Gender {
	Unknown = 0,
	Male = 1,
	Female = 2,
}
```

さて JavaScript に対して **サンプルコードの行数で ~~型の違い~~ 格の違いを見せつけたところで** 、本題に参りましょう。


# IntelliSense でスラスラ書きたい！

C# といえばなんと言っても、型情報からコーディングを支援してくれる **IntelliSense** がめちゃくちゃ機能すると言ったところでしょう。

みなさんも VSCode で TypeScript を書くと、メソッドをいちいち覚えていなくても `.` さえ書けばあとはエディタが列挙してくれた候補の中から「これ！」って選ぶだけっていう便利機能、使ったことはあるかと思います。

TypeScript というか JavaScript では、実質オブジェクトは自由に作ることができて、型宣言しなくてもレコードとかフィールドとかのデータを自由に構築することができます。何なら TypeScript なら型推論が **構造的部分型** に即しているので、オブジェクトのインスタンスは *その型のインスタンスそのものである必要がない* ので自由度が下がることは滅多にありません。

**が。**

C# はそうはいかない。すべてがクラス or 構造体でできていて、いちいち事前に宣言しないといけない。

つまり、 IntelliSense の恩恵を受けようとなるとこうなる。

```c#:悲劇のコード

/// <summary> アプリID:122 取得用レコード </summary>
class App122RecordResponse {
    [JsonProperty("$id")]
    RecordIdResponseField _Id { get; set; }
    [JsonProperty("$revision")]
    RevisionResponseField _Revision { get; set; }
    [JsonProperty("作成者")]
    CreatorResponseField _Creator { get; set; }
    [JsonProperty("作成日時")]
    CreatedDateResponseField _CreatedDate { get; set; }
    [JsonProperty("更新者")]
    ModifierResponseField _Modifier { get; set; }
    [JsonProperty("更新日時")]
    UpdatedDateResponseField _UpdatedDate { get; set; }
    [JsonProperty("ステータス")]
    StatusResponseField _Status { get; set; }
    SingleLineTextResponseField Name { get; set; }
    RadioButtonResponseField Gender { get; set; }
}

/// <summary> アプリID:122 作成用レコード
class App122RecordForRegist {
    SingleLineTextRegistField Name { get; set; } = new();
    RadioButtonRegistField Gender { get; set; } = new();
}

...

/// <summary> 文字列(1行)フィールド 取得用 </summary>
class SingleLineTextResponseField {
    [JsonProperty("type")]
    public string Type { get; set; } = "SINGLE_LINE_TEXT";
    [JsonProperty("value")]
    public string Value { get; set; } = new();
}

/// <summary> 文字列(1行)フィールド 作成用 </summary>
class SingleLineTextRegistField {
    [JsonProperty("value")]
    public string? Value { get; set; }
}

/// <summary> ラジオボタンフィールド 取得用 </summary>
class RadioButtonResponseField {
    [JsonProperty("type")]
    public string Type { get; set; } = "RADIO_BUTTON";
    [JsonProperty("value")]
    public string Value { get; set; } = new();
}

/// <summary> ラジオボタンフィールド 作成用 </summary>
class RadioButtonRegistField {
    [JsonProperty("value")]
    public string? Value { get; set; }
}

...
```

おわかりいただけただろうか。

kintone は **取得してきたときのレコードの形** と **レコードを作成・更新するときの形** が違うのである。 [^2]

フィールドに `type` が含まれていいのかとか、 `$id` が含まれていいとか、そのあたりが API によってまちまちで、下手なことをすると失敗することがある。だから **全部別々にクラスを宣言しないといけない** 。

なんなら日付フィールドなんて値が文字列で来るものだから、安直に `public DateTime Value { get; set; }` って宣言していたら実行時エラーになる可能性もある。だから `Value` は `string` で宣言しておいて、 `public DateTime ValueAsDateTime => DateTime.Parse(this.Value);` みたいなプロパティを別で作っておく羽目になる。


# 汎用クラスを作ろう！

こんなのをアプリごとに書いてたらキリがない！というか型宣言も面倒だし、取得してきた後更新に使おうとしてもいちいち型を使い分けなきゃいけなくて面倒。

確かにフィールドコードは宣言しておかないと補完できないだろうけど、そもそもフィールドコードはアプリによって可変なわけで、設定変更されたら動かないのは分かっているのだから手抜きをしたい！

だから、極力同じ型を使い回せるように **汎用的なクラス** を作ってみましょう。

フィールドコードだけ文字列で指定できるようにして、そのあとは IntelliSense がバリバリ効いてくれるよな、そんな便利な型が欲しい。

幸い、 C# には **インデクサ** という便利な機能があって、 JavaScript みたいに `item["文字列"]` というように書ける仕組みがあります。これで少しは楽に書けるんじゃないかな？

というわけで、こんな感じにしてみる。

```c#:汎用データ型を作る
/// <summary> 汎用レコード型 </summary>
class Record {

    // フィールドコードをなんでも受け付けるようにしておいて…
    protected internal Dictionary<string, Field> _fields = new();

    // インデクサでアクセス可能にする
    public Field this[string fieldCode] {
        get => this._fields[fieldCode];
        set {
            if (_fields.ContainsKey(fieldCode)) {
                _fields[fieldCode] = value;
            } else {
                _fields.Add(fieldCode, value);
            }
        }
    }
}

...

/// <summary> フィールドタイプ </summary>
enum FieldType {
    [EditorBrowsable(EditorBrowsableState.Never)]
    Unknown,
    RECORD_NUMBER,
    __ID__,
    SINGLE_LINE_TEXT,
    MULTI_LINE_TEXT,
    NUMBER,

    ...

}

/// <summary> フィールド </summary>
abstract class Field {

    // フィールドタイプは列挙型にしてプロっぽく
    public abstract FieldType Type { get; }

    // 後書きでキャストできるサポートメソッドをつけて…
    public TField As<TField>() where TField : Field =>
        this is TField f ? f : throw new InvalidCastException();
}

// 各フィールド型の実装でそれぞれ `Value` を実装する
class SingleLineTextField: Field {
    public override FieldType Type { get; set; } = FieldType.SINGLE_LINE_TEXT;
    public string? Value { get; set; }
}

class MultiLineTextField: Field {
    public override FieldType Type { get; set; } = FieldType.MULTI_LINE_TEXT;
    public string? Value { get; set; }
}

// 個別のクラスなので `Value` の型を自由に変更できる
class DateTimeField: Field {
    public override FieldType Type { get; set; } = FieldType.DATETIME;
    public DateTime? Value { get; set; }
}

...

// ついでにリクエストボディ用のクラスも宣言
class PostRecordsRequestParameter {
    public string App { get; set; } = string.Empty;
    public IList<Record> Records { get; set; } = new List<Record>();
}

// PUT は更新キーがあるので別クラス
class PutRecordsRequestParameter {
    public string App { get; set; } = string.Empty;
    public IList<PutRecordsRequestParameterRecord> Records { get; set; } = new List<PutRecordsRequestParameterRecord>();
}

class PutRecordsRequestParameterRecord {
    public bool IsUpdateKey => this.UpdateKey != null;
    public long? Id { get; set; }
    public UpdateKey? UpdateKey { get; set; } = null;
    public long? Revision { get; set; } = null;
    // ここの `Record` は POST と同じ
    public Record Record { get; set; } = new Record();
}

...
```

これでフィールドコードを文字列で扱いながら、フィールドの型がしっかり明示された汎用クラスが出来上がった！

型を明示したいときは `record["文字列_1行_1"].As<SingleLineTextField>().Value` なんてキャストしながら書ける！最高！


# ここで登場ピンチヒッター `System.Text.Json`

で、ここまで来て問題になるのが **どうやってこのクラスに JSON を当てはめるか。** はたまた **どうやって JSON に変換するか。**

たとえば `Record` なんてインデクサを使っているし、 `DateTimeField` 型は `Value` を `DateTime` 型で宣言している。

こんな事したら `System.Text.Json` 君は何をどう JSON に変換していいのやら… という状況になりかねない。


**そこで！！！**

`System.Text.Json` の **カスタムコンバーター[^3]** という仕組みを使ってこれらと JSON の橋渡しをしてやります。

仕組みとしては、 `System.Text.Json` に対して「このクラスを変換するときにはこの処理を使って！」っていうクラスを教えてあげるような形になります。

試しにカスタムコンバーターを含めて `Record` 型を書き直してあげます。

```c#
// 属性でカスタムコンバーターを適用
[JsonConverter(typeof(RecordJsonConverter))]
class Record {

    // 別にインナークラスじゃなくてもいいけど、今回はクラス内部で定義
    // カスタムコンバータは `JsonConverter<T>` という抽象クラスを継承します
    internal class RecordJsonConverter : JsonConverter<Record> {

        // JSON → クラス に変換するメソッドを定義
        public override Record Read(
            ref Utf8JsonReader reader,
            System.Type typeToConvert,
            JsonSerializerOptions? options
            )
        {
            // とりあえず `Dictionary<string, Field>` 型で勢いよく変換
            // フィールドコードみたいに、プロパティ名がわからない場合は
            // 連想配列として扱いたいので `IDictionary<K, V>` の実装クラスでデシリアライズするといい
            var dic = JsonSerializer.Deserialize<Dictionary<string, Field>>(ref reader, options) ?? new Dictionary<string, Field>();

            // 内部クラスだから直接代入しちゃえ
            var record = new Record { _fields = dic, };
            return record;
        }

        // クラス → JSON に変換するメソッドを定義
        public override void Write(
            Utf8JsonWriter writer,
            Record value,
            JsonSerializerOptions? options
            ) => JsonSerializer.Serialize(writer, value._fields, options);
        // まあ…やってることは逆向きなだけだね
    }

    protected internal Dictionary<string, Field> _fields = new();
    public Field this[string fieldCode] {
        get => this._fields[fieldCode];
        set {
            if (_fields.ContainsKey(fieldCode)) {
                _fields[fieldCode] = value;
            } else {
                _fields.Add(fieldCode, value);
            }
        }
    }
}
```

このカスタムコンバーターでは、 `Record` 型のインスタンスは直接 `System.Text.Json` に渡さず、中の `_fields` フィールドだけをやり取りしています。

この方法を使うことで、フィールドコードがわからないアプリでもすべてのフィールド (プロパティ) を連想配列にすることができて、独自の処理を挟み込むことができます。

今回の場合は、 `Record` 型のインデクサ経由で扱うところが独自処理ですね。

他にも、いったん連想配列で受け取っておいてプロパティが生えているかを確認して処理を決定する、なんてこともできたりします。


## さらにすごいカスタムコンバーター

`Field` 型に関してはもっとすごいことをしてしまいましょう。そもそも各フィールドごとにクラスを分けていたので、 JSON の `type` をみて判断しなければなりません。

つまり。判断するカスタムコンバーターを作ればいいってことですね。

```c#:基本Field型
// "既定の" カスタムコンバーターを適用
[JsonConverter(typeof(FieldJsonConverter))]
abstract class Field
{

    // 見よ…これがカスタムコンバーターだッ！！
    internal class FieldJsonConverter : JsonConverter<Field>
    {
        // 見よ…これg (以下略)
        public override Field Read(
            ref Utf8JsonReader reader,
            System.Type typeToConvert,
            JsonSerializerOptions? options
            )
        {
            // 一旦中身をすべて `JsonDocument` に変換します
            // ※ XmlDocument の Json 版みたいな。
            var json = JsonDocument.ParseValue(ref reader);

            // `reader` は一回読み取ってカーソルが動いてしまったので
            // もう一度 JSON に変換しなおしておきます
            // ※ここの処理もっと良い形があれば教えてください…
            var raw = json.RootElement.GetRawText();

            // そして、ここで `type` を判別！
            var typeRaw = json.RootElement.GetProperty("type").GetString();

            // 列挙型に文字列からパース
            if (Enum.TryParse<FieldType>(typeRaw, out var type))
            {
                // パースが成功したら各フィールド型での変換処理に振り分け
                // さっき JSON にしなおしたテキストを各フィールド型のコンバータで変換します
                // つまり各フィールドごとに更なるカスタムコンバーターが存在するわけですね… (重そう)(小並感)
                return (Field?)(type switch
                {
                    FieldType.RECORD_NUMBER => JsonSerializer.Deserialize<RecordNumberField>(raw),
                    FieldType.__ID__ => JsonSerializer.Deserialize<RecordIdField>(raw),
                    FieldType.__REVISION__ => JsonSerializer.Deserialize<RevisionField>(raw),
                    FieldType.CREATOR => JsonSerializer.Deserialize<CreatorField>(raw),
                    FieldType.CREATED_TIME => JsonSerializer.Deserialize<CreatedTimeField>(raw),
                    FieldType.MODIFIER => JsonSerializer.Deserialize<ModifierField>(raw),
                    FieldType.UPDATED_TIME => JsonSerializer.Deserialize<UpdatedTimeField>(raw),
                    FieldType.SINGLE_LINE_TEXT => JsonSerializer.Deserialize<SingleLineTextField>(raw),
                    FieldType.NUMBER => JsonSerializer.Deserialize<NumberField>(raw),
                    FieldType.CALC => JsonSerializer.Deserialize<CalcField>(raw),
                    FieldType.MULTI_LINE_TEXT => JsonSerializer.Deserialize<MultiLineTextField>(raw),
                    FieldType.RICH_TEXT => JsonSerializer.Deserialize<RichTextField>(raw),
                    FieldType.CHECK_BOX => JsonSerializer.Deserialize<CheckBoxField>(raw),
                    FieldType.RADIO_BUTTON => JsonSerializer.Deserialize<RadioButtonField>(raw),
                    FieldType.DROP_DOWN => JsonSerializer.Deserialize<DropDownField>(raw),
                    FieldType.MULTI_SELECT => JsonSerializer.Deserialize<MultiSelectField>(raw),
                    FieldType.FILE => JsonSerializer.Deserialize<AttachmentFileField>(raw),
                    FieldType.LINK => JsonSerializer.Deserialize<LinkField>(raw),
                    FieldType.DATE => JsonSerializer.Deserialize<DateField>(raw),
                    FieldType.TIME => JsonSerializer.Deserialize<TimeField>(raw),
                    FieldType.DATETIME => JsonSerializer.Deserialize<DateTimeField>(raw),
                    FieldType.USER_SELECT => JsonSerializer.Deserialize<UserSelectField>(raw),
                    FieldType.CATEGORY => JsonSerializer.Deserialize<CategoryField>(raw),
                    FieldType.STATUS => JsonSerializer.Deserialize<StatusField>(raw),
                    FieldType.STATUS_ASSIGNEE => JsonSerializer.Deserialize<StatusAssigneeField>(raw),
                    FieldType.SUBTABLE => JsonSerializer.Deserialize<SubTableField>(raw),
                    FieldType.ORGANIZATION_SELECT => JsonSerializer.Deserialize<OrganizationSelectField>(raw),
                    FieldType.GROUP_SELECT => JsonSerializer.Deserialize<GroupSelectField>(raw),
                    // 念のためフォールバックもご用意
                    _ => JsonSerializer.Deserialize<UnknownField>(raw),
                }) ?? new UnknownField();
            }
            else
            {
                return new UnknownField();
            }
        }

        // クラス → JSON への変換は System.Text.Json に型情報を渡してあげればよいので、
        // `typeof value` もしくは `value.GetType()` で型情報を取得して一緒に渡してあげる
        // すると各フィールド型のカスタムコンバーターで処理される
        public override void Write(
            Utf8JsonWriter writer,
            Field value,
            JsonSerializerOptions? options
            ) => JsonSerializer.Serialize(writer, value, value.GetType(), options);
    }

    // こ、こいつは…なんだッ！？
    internal class PostFieldJsonConverter : FieldJsonConverter
    {
        public override void Write(
            Utf8JsonWriter writer,
            Field value,
            JsonSerializerOptions? options
            )
        {
            // POST や PUT の際に `type` は不要…
            // 故に `value` だけを変換するのじゃ…
            writer.WriteStartObject();
            writer.WritePropertyName("value");

            // フィールド側に value だけ JSON に変換するメソッドを用意しておけば
            // 問題ないのだ…フフフ…(?)
            value.SerializeValue(writer, options);
            writer.WriteEndObject();
        }

        // JSON → クラスには変換しない前提だから、
        // `Read` メソッドはオーバーライドしない…だとッ！？
        // 生意気な…ッ！
    }

    // ここで継承するクラスに実装を強制させる…ってコト！？
    // なかなかやるじゃねぇか…
    internal protected abstract void SerializeValue(Utf8JsonWriter writer, JsonSerializerOptions? options);

    public abstract FieldType Type { get; }
    public TField As<TField>() where TField : Field => this is TField f ? f : throw new InvalidCastException();
}
```

```c#:個別のField型
class SingleLineTextField: Field {

    // 実はオプションで列挙型の名前で変換することもできる
    [JsonPropertyName("type")]
    public override FieldType Type { get; } = FieldType.SINGLE_LINE_TEXT;

    [JsonPropertyName("value")]
    public string? Value { get; set; } = null;

    // `Value` だけ変換するメソッドを実装
    internal protected override void SerializeValue(
        Utf8JsonWriter writer,
        JsonSerializerOptions? options)
        => JsonSerializer.Serialize(writer, this.Value, options);

}

// 同じ要領で文字列(複数行)にも対応！
class MultiLineTextField: Field {
    [JsonPropertyName("type")]
    public override FieldType Type { get; } = FieldType.MULTI_LINE_TEXT;
    [JsonPropertyName("value")]
    public string? Value { get; set; } = null;
    internal protected override void SerializeValue(
        Utf8JsonWriter writer,
        JsonSerializerOptions? options)
        => JsonSerializer.Serialize(writer, this.Value, options);
}

// 日付フィールドの場合は…
class DateTimeField: Field {

    [JsonPropertyName("type")]
    public override FieldType Type { get; } = FieldType.DATETIME;

    // DateTime 用の変換クラスを用意！
    class DateTimeJsonConverter : JsonConverter<DateTime?> {
        public override DateTime? Read(
            ref Utf8JsonReader reader,
            System.Type typeToConvert,
            JsonSerializerOptions? options
            )
        {
            var value = reader.GetString();
            return string.IsNullOrEmpty(value) ? null : DateTime.Parse(value, null, DateTimeStyles.RoundtripKind);
        }

        public override void Write(
            Utf8JsonWriter writer,
            DateTime? value,
            JsonSerializerOptions? options
            )
        {
            if (value.HasValue)
                writer.WriteStringValue(value.Value.ToString("yyyy-MM-ddTHH:mm:ssK"));
            else
                writer.WriteNullValue();
        }
    }

    // これで安心して変換できますね！
    [JsonPropertyName("value")]
    [JsonConverter(typeof(DateTimeJsonConverter))]
    public string? Value { get; set; } = null;

    // Value だけ変換するときもカスタムコンバータを実体化すればOK
    internal protected override void SerializeValue(
        Utf8JsonWriter writer,
        JsonSerializerOptions? options)
        => new N6eISO8601DateTimeStringJsonConverter().Write(writer, this.Value, options);
}
```

```c#:リクエストボディも…
// POST リクエスト用のリクエストボディクラス
class PostRecordsRequestParameter {

    [JsonPropertyName("app")]
    public string App { get; set; } = string.Empty;
    [JsonPropertyName("records")]
    public IList<Record> Records { get; set; } = new List<Record>();

    // ん…？このクラスはカスタムコンバータを定義していない…？
    public string ToJson() => JsonSerializer.Serialize(
        this,
        new JsonSerializerOptions {
            Converters = {
                new Field.PostFieldJsonConverter(), // ここでフィールドのカスタムコンバータは定義しているが…
            },
            Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
        });
}

// PUT リクエスト用のリクエストボディクラス
class PutRecordsRequestParameter {

    [JsonPropertyName("app")]
    public string App { get; set; } = string.Empty;
    [JsonPropertyName("records")]
    public IList<PutRecordsRequestParameterRecord> Records { get; set; } = new List<PutRecordsRequestParameterRecord>();

    // 実はね…カスタムコンバータが面倒になっちゃったんだ…()
    // だからメソッドで変換してみたんだ…
    public string ToJson()
    {
        // 気分で `Utf8JsonWriter` でも使ってみますか…
        using var stream = new MemoryStream();
        using var writer = new Utf8JsonWriter(
            stream,
            new JsonWriterOptions
            {
                Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
                Indented = false,
            });

        writer.WriteStartObject();
        writer.WriteString("app", this.App);
        writer.WritePropertyName("records");
        JsonSerializer.Serialize(
            writer, 
            this.Records,
            new JsonSerializerOptions {
                Converters = {
                    new Field.PostFieldJsonConverter(),
                },
                Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
            });
        writer.WriteEndObject();

        // これ忘れると痛い目にあいます (あった)
        writer.Flush();
        return Encoding.UTF8.GetString(stream.ToArray());
    }
}

```

はい、みなさんお分かりの通り **相当な力技です。**

基底フィールド型のカスタムコンバータにフィールド種類の判断ロジックを仕込んでおいて、 `Record` 型のカスタムコンバータから *わざと* 基底フィールド型のカスタムコンバータを使うように仕向け、その中で個別のフィールド型の変換ロジックを呼び出す。

個別のフィールド型自体にはカスタムコンバータは無いけれど、フィールドに対してカスタムコンバータを割り当てることができるので、結果的に C# 側での `Value` の型にあわせた変換がおこなわれる。

さらにレコードを登録するような POST / PUT リクエストの場合、内部で *わざと* `type` を無視して変換するようなカスタムコンバータを定義しておいて、リクエストボディ側のクラスのカスタムコンバータから *わざわざ* カスタムコンバータを指定して変換を行います。

もう何回「カスタムコンバータ」って書いたかわかりませんね。もはや最後なんて諦めちゃってるし。…でも、これをすべてのフィールドで用意することで、下のような書き方で JSON に変換ができるようになります。

```c#
var record1 = new Record {
    // 文字列(1行) … string
    ["field1"] = new SingleLineTextField { Value = "text1", },
    // 数値 … decimal
    ["field2"] = new NumberField { Value = 100.5m, },
    // チェックボックス … List<string>
    ["チェックボックス"] = new CheckBoxField { Value = { "sample1", "sample3", }, },
    // 日時 … DateTime
    ["field4"] = new DateTimeField { Value = new DateTime(2020, 1, 3, 10, 56, 30, DateTimeKind.Utc), },
    // 日付 … DateTime
    ["field5"] = new DateField { Value = new DateTime(2020, 1, 3, 10, 56, 30, DateTimeKind.Local), },
    // 時刻 … TimeSpan
    ["field6"] = new TimeField { Value = new TimeSpan(10, 56, 30), },
};

var jsonRecord1 = JsonSerializer.Serialize(record1);
// {"field1":{"type":"SINGLE_LINE_TEXT","value":"text1"},"field2":{"type":"NUMBER","value":"100.5"},"\u30C1\u30A7\u30C3\u30AF\u30DC\u30C3\u30AF\u30B9":{"type":"CHECK_BOX","value":["sample1","sample3"]},"field4":{"type":"DATETIME","value":"2020-01-03T10:56:30Z"},"field5":{"type":"DATE","value":"2020-01-03"},"field6":{"type":"TIME","value":"10:56"}}


var record2 = new Record {
    // 実は文字列にも対応
    ["field4"] = new DateTimeField { Value = "2020-01-03T10:56:30+09:00", },
    ["field5"] = new DateField { Value = "2020-01-03", },
    ["field6"] = new TimeField { Value = "10:56", },
};

var jsonRecord2 = JsonSerializer.Serialize(record2);
// {"field4":{"type":"DATETIME","value":"2020-01-03T10:56:30\u002B09:00"},"field5":{"type":"DATE","value":"2020-01-03"},"field6":{"type":"TIME","value":"10:56"}}


var record3 = new Record {
    ["field4"] = new DateTimeField { Value = "2020-01-03T10:56:30Z", },
};

var jsonRecord3 = JsonSerializer.Serialize(record3);
// {"field4":{"type":"DATETIME","value":"2020-01-03T10:56:30Z"}}


var record4 = new Record {
    // サブテーブル
    ["subtable"] = new SubTableField {
        Value = {
            // 行IDなし
            new SubtableRow {
                Value = {
                    ["subfield1"] = new SingleLineTextField { Value = "row1", },
                },
            },
            // 行IDあり
            new SubtableRow {
                Id = "123",
                Value = {
                    ["subfield1"] = new SingleLineTextField { Value = "row2", },
                },
            },
        },
    },
};

var jsonRecord4 = JsonSerializer.Serialize(record4);
// {"subtable":{"type":"SUBTABLE","value":[{"value":{"subfield1":{"type":"SINGLE_LINE_TEXT","value":"row1"}}},{"id":"123","value":{"subfield1":{"type":"SINGLE_LINE_TEXT","value":"row2"}}}]}}
var json = request.ToJson();


var request = new PostRecordsRequestParameter {
    App = "100",
    Records = { record1, record2, record3, record4 },
};

// {"app":"100","records":[{"field1":{"value":"text1"},"field2":{"value":"100.5"},"チェックボックス":{"value":["sample1","sample3"]},"field4":{"value":"2020-01-03T10:56:30Z"},"field5":{"value":"2020-01-03"},"field6":{"value":"10:56"}},{"field4":{"value":"2020-01-03T10:56:30+09:00"},"field5":{"value":"2020-01-03"},"field6":{"value":"10:56"}},{"field4":{"value":"2020-01-03T10:56:30Z"}},{"subtable":{"value":[{"value":{"subfield1":{"value":"row1"}}},{"id":"123","value":{"subfield1":{"value":"row2"}}}]}}]}
```



# まとめ

というわけで断片的なコードにはなりますが、 C# だけでも頑張れば kintone のレコードの JSON をパース・シリアライズできるということがお判りいただけたと思います。

すべてのソースをお見せすることができないので残念ですが、興味を持った方はぜひこのサンプルコードをもとにして、ライブラリを自作してみてください。

以上です。お読みいただきありがとうございました。


[^1]: 最近このあたり仕様が変わったみたいで、追いつけてないんですよね…気づいたら null 安全になってるし… https://docs.microsoft.com/ja-jp/dotnet/standard/serialization/system-text-json-configure-options
[^2]: この記事がわかりやすいと思います: https://developer.cybozu.io/hc/ja/articles/202166330
[^3]: マイクロソフト公式からも記事が出ているのでぜひチェックしてください: https://docs.microsoft.com/ja-jp/dotnet/standard/serialization/system-text-json-converters-how-to