CodeGym /コース /C# SELF /System.Text.Json を深掘りする Sy...

System.Text.Json を深掘りする System.Text.Json

C# SELF
レベル 47 , レッスン 1
使用可能

1. はじめに

このレッスンでは「ただリストを保存したい」っていう段階から一歩進んで、System.Text.Json を使った柔軟で正確、そして高性能なシリアライゼーションの話をします。現代の .NET プロジェクトでは JSON が事実上のデータ交換標準です。基本的な Serialize/Deserialize の呼び出しは簡単ですが、実際の要件は細かい設定を求めます:フィールドの無視/リネーム、日付フォーマットの管理、循環対策、大容量データの扱い、カスタムコンバーターなど。

ここでは JsonSerializer のメソッドだけでなく、JsonSerializerOptions 経由の設定、属性、Stream を使った操作、メモリ管理、そして JsonConverter を通した独自のシリアライズ規則の導入についても見ていきます。

簡単な歴史と位置づけ:System.Text.Json

長い間 .NET では Newtonsoft.Json (Json.NET) が主流でした — 柔軟で成熟していますが、必ずしも最速でも最軽量でもありません。 .NET Core 3.0 からは組み込みの System.Text.Json が登場しました:高いパフォーマンス、依存関係の最小化(プラットフォームに含まれている)、ASP.NET Core との密な統合、そして .NET のリリースとともに継続的に進化しています。

2. 基本クラスとメソッド

基本要素は静的クラスの JsonSerializer で、主に二つの方向を提供します:

  • シリアライズ:オブジェクト → JSON 文字列 (Serialize)
  • デシリアライズ:JSON 文字列 → 必要な型のオブジェクト (Deserialize)

シンプルなオブジェクトのシリアライズ例

using System.Text.Json;

var person = new Person { Name = "イワン", Age = 30 };
string jsonString = JsonSerializer.Serialize(person);
Console.WriteLine(jsonString); // {"Name":"イワン","Age":30}

デシリアライズの例

var json = "{\"Name\":\"アッナ\",\"Age\":22}";
var anna = JsonSerializer.Deserialize<Person>(json);
Console.WriteLine(anna.Name); // アッナ

注:型 Person は前の講義で実装済みのものを使います — ここでも同じ型を使います。

3. シリアライゼーションの管理: JsonSerializerOptions

実プロジェクトではほぼ常に設定が必要です:プロパティ名を camelCase にする、日付フォーマット、循環の扱い、デフォルト値の扱いなど。これらはすべて JsonSerializerOptions で制御します。

設定の例

var options = new JsonSerializerOptions
{
    WriteIndented = true,    // JSON を見やすくフォーマット(スペースと改行を追加)
    PropertyNameCaseInsensitive = true, // デシリアライズ時にプロパティ名の大文字小文字を無視
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase // プロパティを camelCase にする(PascalCase ではなく)
};

string json = JsonSerializer.Serialize(person, options);
/*
{
  "name": "イワン",
  "age": 30
}
*/

なぜ重要か?多くのフロントエンドフレームワークは camelCase を期待していて、.NET の標準である PascalCase とは異なることが多いからです。

4. 属性: System.Text.Json.Serialization

モデル側に属性を付けてシリアライズを制御する方が楽な場合もあります。属性はフィールドやプロパティに付けて、名前の変更、含める/除外、値の処理などに影響を与えます。

主な属性

属性 何をするか
[JsonIgnore]
プロパティをシリアライズ/デシリアライズから除外する
[JsonPropertyName("名前")]
JSON で別名を使う
[JsonInclude]
非公開のプロパティ/フィールドをシリアライズに含める
[JsonNumberHandling]
数値の扱いを制御する

例:属性でプロパティを制御する

using System.Text.Json.Serialization;

public class Person
{
    [JsonPropertyName("full_name")]
    public string Name { get; set; }

    [JsonIgnore]
    public int SecretCode { get; set; }

    public int Age { get; set; }
}
var person = new Person { Name = "ピョートル", Age = 45, SecretCode = 123 };
string json = JsonSerializer.Serialize(person);
// {"full_name":"ピョートル","Age":45}

注意:SecretCode は JSON に含まれておらず、Name"full_name" としてシリアライズされています。

5. コレクションとネストしたオブジェクトのシリアライズ

コレクションは簡単

var numbers = new List<int> { 1, 2, 3 };
string json = JsonSerializer.Serialize(numbers); // [1,2,3]

var people = new List<Person> {
    new Person { Name = "アッナ", Age = 20 },
    new Person { Name = "マクシム", Age = 40 }
};
string jsonList = JsonSerializer.Serialize(people);
// [{"Name":"アッナ","Age":20},{"Name":"マクシム","Age":40}]

ネストした構造

public class Group
{
    public string Name { get; set; }
    public List<Person> Members { get; set; }
}

var group = new Group
{
    Name = "開発者",
    Members = new List<Person>
    {
        new Person { Name = "サーシャ", Age = 23 },
        new Person { Name = "マーシャ", Age = 28 }
    }
};

string jsonGroup = JsonSerializer.Serialize(group, options);
/*
{
  "name": "開発者",
  "members": [
    { "name": "サーシャ", "age": 23 },
    { "name": "マーシャ", "age": 28 }
  ]
}
*/

6. デシリアライズ:知っておくべきこと

var json = "[{\"Name\":\"イワン\",\"Age\":21}]";
var list = JsonSerializer.Deserialize<List<Person>>(json);
Console.WriteLine(list[0].Name); // イワン

よくあるシナリオ:JSON にフィールドが無い場合、対応するプロパティはデフォルト値を受け取ります。JSON にあってモデルに無い余分なフィールドは無視されます。しかし型が一致しない(例えば数の代わりに文字列が来る)場合は、デシリアライズ時に例外が投げられます。

7. 日付、時間、フォーマット、数値の扱い

public class Meeting
{
    public string Topic { get; set; }
    public DateTime Time { get; set; }
}

var meeting = new Meeting { Topic = "ミーティング", Time = DateTime.Now };
string json = JsonSerializer.Serialize(meeting);
// {"Topic":"ミーティング","Time":"2024-06-06T20:30:00.0000000+03:00"}

デフォルトでは DateTime は ISO 8601 でシリアライズされます。別の形式(例えば日付だけ)を使いたいなら、別プロパティを用意するかカスタムコンバーターを使ってください(後述)。

FAQ: 数値を文字列としてシリアライズしたい(電話番号や大きな ID など)の場合は、属性 [JsonNumberHandling(JsonNumberHandling.WriteAsString)] を使います。

8. ストリームとファイル操作

文字列だけでなく Stream を使って操作できます — 大きなデータ(ファイルやネットワーク)では重要です。

ファイルへの書き込み例

using var fs = File.Create("person.json");
JsonSerializer.Serialize(fs, person);
// fs.Flush() を呼ぶか using を使うのを忘れないで!

ファイルからの読み込み例

using var fs = File.OpenRead("person.json");
var restored = JsonSerializer.Deserialize<Person>(fs);

ストリームには非同期メソッド SerializeAsync/DeserializeAsync もあり、高負荷サービスで役に立ちます。

9. カスタムコンバーター

標準ルールで足りない場合(非標準の日付/数値フォーマット、複雑な値、独自の構造など)は、JsonConverter を実装します。

例:日付を "dd.MM.yyyy" のみで扱うコンバーター

public class CustomDateConverter : JsonConverter<DateTime>
{
    public override DateTime Read(
        ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        return DateTime.ParseExact(reader.GetString(), "dd.MM.yyyy", null);
    }

    public override void Write(
        Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
    {
        writer.WriteStringValue(value.ToString("dd.MM.yyyy"));
    }
}

var options = new JsonSerializerOptions();
options.Converters.Add(new CustomDateConverter());

var dt = new DateTime(2024, 6, 1);
string json = JsonSerializer.Serialize(dt, options); // "01.06.2024"

カスタムコンバーターは座標、ベクトル、色、非標準の日付や通貨、独自の ID フォーマットなどのシリアライズで便利です。

10. 便利な注意点

循環参照と深い階層の処理

var options = new JsonSerializerOptions
{
    ReferenceHandler = ReferenceHandler.Preserve, // 全オブジェクトを $id/$ref で保存する
    WriteIndented = true
};

重要:このオプションを使うと JSON に内部的なプロパティ $id$ref が出てきます。外部システムがそれらを理解しない場合は向かないことがあります。

System.Text.JsonNewtonsoft.Json の違い

System.Text.Json はかなり強力になっていますが、まだ Newtonsoft.Json がカバーするすべてのシナリオ(例えばプライベートコンストラクタ、複雑な動的オブジェクト等)を完全には網羅していません。多くの標準的なタスクには組み込みのシリアライザをお勧めします — こちらの方が速く、余計な依存がありません。

JSON を対話的に扱う:DOM API

完全なモデルを用意せずに JSON を「つぶしていく」必要があるときは、JsonDocumentJsonElement を使ってください。

using var doc = JsonDocument.Parse(jsonString);
JsonElement root = doc.RootElement;

if (root.TryGetProperty("Name", out var nameProperty))
{
    Console.WriteLine(nameProperty.GetString());
}

11. 便利なオプションとその効果

プロパティ 値/用途
WriteIndented
true — インデント付きでフォーマットする
PropertyNameCaseInsensitive
true — デシリアライズ時にプロパティ名の大文字小文字を無視する
PropertyNamingPolicy
JsonNamingPolicy.CamelCase
DefaultIgnoreCondition
null/デフォルト値を無視するルール
ReferenceHandler
Preserve
,
IgnoreCycles
AllowTrailingCommas
true — 配列の末尾のカンマを許可する
NumberHandling
数値を文字列に変換/戻すなどの処理
Converters
カスタムコンバーターのリスト

12. よくあるミスと実践的アドバイス

ミス #1:デシリアライズ時の型が間違っている。 リストをシリアライズしたなら必ずリストにデシリアライズする:List<T> のように、単一オブジェクトではなく。

ミス #2:プロパティ名の大文字小文字を間違える。 設定しないとプロパティが「見つからない」ことがあります。PropertyNameCaseInsensitive を使うか、PropertyNamingPolicy を適切に設定してください。

ミス #3:日付の扱いが間違っている。 デフォルトは ISO 8601 です。別フォーマットが必要ならコンバーター(JsonConverter<DateTime>)を書いて適用してください。

ミス #4:プライベート/静的フィールドのシリアライズを期待している。 デフォルトではパブリックなプロパティが対象です。特殊ケースでは属性(例:[JsonInclude])を使ってください。

ミス #5:デフォルト値の理解不足。 JSON にフィールドが無ければ → プロパティはデフォルト値になります。ロジックでそれを考慮してください。

ミス #6:ストリームの扱いが不適切。 リソースは usingawait using で必ずクローズして、リークやブロックを避けてください。

コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION