【C#】JSONデータをクラスへ変換するベストプラクティスとSystem.Text.Json・Newtonsoft.Json比較
JSONをC#クラスへ変換するなら、Visual Studioの「JSONをクラスとして貼り付け」やJSON2CSharpでモデルを生成し、System.Text.Json
またはNewtonsoft.Json
のDeserializeで読み込む方法が最も手軽で保守もしやすい。
プロパティ名差異はオプションで吸収でき、dynamicより型安全に扱えるので安心です。
JSON変換の全体像
C#でJSONデータを扱う際、JSON文字列をC#のクラスに変換することは非常に重要な作業です。
JSONはWeb APIのレスポンスや設定ファイル、データ保存など幅広い場面で使われており、これを適切にC#のオブジェクトとして扱うことで、プログラムの可読性や保守性が向上します。
このセクションでは、JSON変換の全体像を理解するために、変換が必要になる代表的なユースケースと、変換を行う際のワークフローの違いについて解説します。
変換が必要になるユースケース
JSONデータをC#のクラスに変換する必要がある場面は多岐にわたります。
代表的なユースケースをいくつか挙げてみましょう。
- Web APIとの連携
RESTful APIやGraphQL APIなど、多くのWebサービスはJSON形式でデータを送受信します。
APIから取得したJSONレスポンスをC#のクラスに変換することで、型安全にデータを扱えます。
例えば、ユーザー情報や商品データ、注文履歴などをAPIから取得し、アプリケーション内で処理する際に必須です。
- 設定ファイルの読み込み
アプリケーションの設定情報をJSONファイルで管理するケースも増えています。
設定ファイルをC#のクラスに変換して読み込むことで、設定値のアクセスが簡単になり、誤った値の入力を防ぐことができます。
- データの永続化と復元
JSONは軽量なデータフォーマットとして、データの保存や復元に使われます。
例えば、ユーザーの操作履歴やアプリケーションの状態をJSONで保存し、再起動時に復元する場合、JSONをC#のクラスに変換して扱います。
- ログやイベントデータの解析
ログやイベント情報をJSON形式で出力し、後から解析するケースもあります。
JSONをC#のクラスに変換して解析処理を行うことで、効率的にデータを処理できます。
- クロスプラットフォーム開発
XamarinやUnityなどのクロスプラットフォーム環境でもJSONは標準的なデータ交換フォーマットです。
これらの環境でJSONをC#のクラスに変換して扱うことは、データの整合性を保つうえで重要です。
これらのユースケースに共通するのは、JSONデータをプログラム内で安全かつ効率的に扱うために、C#のクラスに変換する必要があるという点です。
型情報を持つクラスに変換することで、IDEの補完機能やコンパイル時の型チェックが活用でき、バグの早期発見やコードの保守性向上につながります。
ワークフローの比較: 自動生成 vs 手動実装
JSONデータをC#のクラスに変換する際のワークフローは、大きく分けて「自動生成」と「手動実装」の2つの方法があります。
それぞれの特徴やメリット・デメリットを理解して、プロジェクトの要件に合った方法を選択することが重要です。
自動生成のワークフロー
自動生成は、JSONの構造を解析してC#のクラス定義を自動的に作成する方法です。
Visual Studioの「JSONをクラスとして貼り付け」機能や、オンラインツールのJSON2CSharpなどが代表例です。
- メリット
- 作業時間を大幅に短縮できる
- 複雑なネスト構造や配列も自動的にクラス化できる
- JSONの構造変更に対して迅速に対応可能
- 初心者でも簡単にモデルクラスを作成できる
- デメリット
- 自動生成されたクラスの命名規則や型が必ずしも最適でない場合がある
- 不要なプロパティや型の誤認識が発生することがある
- 生成後に手動で修正が必要になることが多い
- JSONのスキーマが頻繁に変わる場合、管理が煩雑になることがある
手動実装のワークフロー
手動実装は、JSONの構造を理解したうえで、C#のクラスを自分で設計・実装する方法です。
特に、APIの仕様書やJSONスキーマが明確にある場合に有効です。
- メリット
- クラス設計を自由にカスタマイズできる
- 命名規則や型の選択をプロジェクトのコーディング規約に合わせられる
- 不要なプロパティを省略し、必要なバリデーションや属性を付与できる
- 複雑な継承やポリモーフィズムを適切に設計できる
- デメリット
- 作成に時間がかかる
- JSONの構造変更に対応するために手動で修正が必要
- 初心者には設計が難しい場合がある
- 大規模なJSON構造の場合、ミスや抜け漏れが発生しやすい
ワークフロー選択のポイント
項目 | 自動生成 | 手動実装 |
---|---|---|
作業時間 | 短い | 長い |
柔軟性 | 低い | 高い |
メンテナンス性 | JSON変更時に再生成が必要 | 変更に応じて手動修正 |
コーディング規約適合 | 要修正 | 容易に適合可能 |
複雑な構造対応 | 限定的 | 高度に対応可能 |
プロジェクトの規模やJSONの複雑さ、メンテナンス体制によって適切な方法を選ぶことが大切です。
例えば、APIのレスポンスが頻繁に変わる開発初期段階では自動生成を活用し、安定期に入ったら手動でクラスを整備するというハイブリッドな運用も効果的です。
また、最近ではソースジェネレーターを活用してビルド時にクラスを自動生成する方法も登場しており、手動実装の柔軟性と自動生成の効率性を両立する選択肢も増えています。
このように、JSON変換のワークフローは単に「自動か手動か」という二択ではなく、プロジェクトの状況や要件に応じて最適な方法を選択し、必要に応じて組み合わせることが望ましいです。
クラス生成テクニック
Visual Studio「JSONをクラスとして貼り付け」
手順と注意点
Visual Studioには、JSON文字列をコピーしてクラスファイルに貼り付けるだけで、自動的にC#のクラス定義を生成する便利な機能があります。
手順は以下の通りです。
- JSON文字列をコピーする
例として、以下のJSONをコピーします。
{
"person": [
{
"name": "penta",
"age": 25
},
{
"name": "ponta",
"age": 40
}
],
"count": 2
}
- Visual Studioで新しいクラスファイルを作成する
例: SampleJson.cs
- クラスファイルを開き、メニューから「編集」→「形式を選択して貼り付け」→「JSONをクラスとして貼り付け」を選択する
- 自動生成されたクラスが貼り付けられる
この機能はJSONの構造を解析し、ネストされたオブジェクトや配列も適切にクラスや配列型として生成します。
- プロパティ名はJSONのキーを元に生成されますが、C#の命名規則に合わない場合は手動で修正が必要です。例えば、JSONのキーがスネークケースの場合、PascalCaseに直すことが多いです
- 型推論は基本的にJSONの値から行われますが、nullが混在する場合や数値が混在する場合は
object
型やnullable型になることがあります。必要に応じて型を明示的に修正してください - 大文字小文字の違いに注意してください。JSONのキーが小文字で、C#のプロパティが大文字で始まる場合、デシリアライズ時に
JsonPropertyName
属性を付けるか、JsonSerializerOptions
で大文字小文字を無視する設定を行う必要があります - 配列の要素が空の場合や、JSONの一部しか貼り付けない場合は、生成されるクラスが不完全になることがあります。完全なJSON構造を貼り付けることが望ましいです
部分的なプロパティ生成のコツ
JSONの一部だけを使ってクラスを生成したい場合、以下のポイントを押さえると効率的です。
- 必要な部分だけを切り出す
例えば、JSON全体が大きい場合、必要なオブジェクトや配列の部分だけをコピーして貼り付けると、その部分に対応したクラスだけが生成されます。
- 配列の要素を1つだけ貼り付ける
配列の中身が複数ある場合、1つの要素だけを貼り付けると、その要素の構造に基づいたクラスが生成されます。
複数の要素で型が異なる場合は注意が必要です。
- nullや空文字列を含める
nullや空文字列を含めることで、nullable型やstring型として正しく推論されやすくなります。
- 手動でJSONを編集してから貼り付ける
例えば、不要なプロパティを削除したり、型を揃えたりしてから貼り付けると、生成されるクラスがシンプルになります。
これらのコツを活用すると、必要なクラスだけを効率よく生成でき、後からの修正も少なくなります。
オンラインツールを使ったモデル生成
JSON2CSharpの活用法
JSON2CSharpはブラウザ上でJSONを貼り付けるだけで、対応するC#クラスを生成してくれる無料のオンラインツールです。
使い方は簡単です。
- JSON2CSharpのサイト(https://json2csharp.com/)にアクセスする
- 左側の入力欄にJSON文字列を貼り付ける
- 「Convert」ボタンを押すと、右側にC#のクラスコードが表示される
- 表示されたコードをコピーしてプロジェクトに貼り付ける
このツールはVisual Studioの機能と同様に、ネストされたオブジェクトや配列も適切にクラス化します。
さらに、複数のクラスが自動的に生成されるため、複雑なJSON構造にも対応可能です。
活用のポイント
- JSONの構造が複雑な場合でも、ツールが自動的にクラスを分割してくれるため、手動でクラスを分割する手間が省けます
- 生成されたクラスは標準的なC#の命名規則に従っているため、そのまま使いやすいです
- ただし、プロパティ名の大文字小文字や型の調整は必要に応じて行ってください
- JSONの一部だけを貼り付けて部分的なクラスを生成することも可能です
他ツールとの比較
JSON2CSharp以外にも、JSONからC#クラスを生成するオンラインツールやVisual Studioの拡張機能が存在します。
代表的なものを比較します。
ツール名 | 特徴 | メリット | デメリット |
---|---|---|---|
JSON2CSharp | シンプルで使いやすい | 無料で即時生成、複数クラス対応 | UIがシンプルすぎて高度な設定なし |
QuickType | 多言語対応、詳細設定可能 | 型推論が強力、カスタマイズ性高い | UIがやや複雑、英語中心 |
Visual Studio拡張機能 (e.g. Paste JSON as Classes) | IDE内で完結 | 開発環境から離れずに生成可能 | Visual Studio依存、機能限定的 |
JsonSchema2Pojo (Java向けだがC#対応もあり) | JSON Schemaから生成 | スキーマベースで厳密 | JSON Schemaが必要、設定が複雑 |
用途や好みによって使い分けると良いでしょう。
特に、単純なJSONならJSON2CSharpが手軽でおすすめです。
Source Generatorによるモデル自動生成
dotnet CLIでの設定
C#のSource Generator機能を使うと、ビルド時にJSONスキーマやJSONサンプルから自動的にクラスを生成できます。
これにより、手動でクラスを作成する手間を減らしつつ、型安全なコードを保てます。
Source Generatorを利用するには、まず対応するNuGetパッケージをプロジェクトに追加します。
例えば、System.Text.Json
のソースジェネレーターを使う場合は以下のようにします。
dotnet add package System.Text.Json.SourceGeneration --prerelease
次に、JsonSerializable
属性を付与したクラスを作成し、生成対象の型を指定します。
using System.Text.Json.Serialization;
[JsonSerializable(typeof(Root))]
public partial class RootJsonContext : JsonSerializerContext
{
}
この設定により、Root
クラスのシリアライズ・デシリアライズ用コードがビルド時に自動生成されます。
ビルド時に生成されるクラスの確認方法
ビルドを実行すると、obj
フォルダ内に自動生成されたソースコードが出力されます。
Visual Studioの「ソリューションエクスプローラー」で「隠しファイルを表示」設定にすると、生成されたファイルを確認できます。
生成されたコードは通常、*.g.cs
や*.Generated.cs
という名前で保存され、partial
クラスとして定義されています。
これにより、元のクラスと連携して動作します。
生成コードを確認することで、どのようなシリアライズ処理が行われているか把握でき、パフォーマンスチューニングやトラブルシューティングに役立ちます。
Source Generatorは特に大規模プロジェクトやパフォーマンスが重要な場面で効果を発揮します。
ビルド時にコードが生成されるため、ランタイムのリフレクションを使わず高速に動作します。
クラス設計の重要ポイント
PascalCaseとcamelCaseのマッピング
C#の命名規則では、クラスやプロパティ名はPascalCase(単語の先頭を大文字)で記述するのが一般的です。
一方、JSONのプロパティ名はcamelCase(先頭小文字)やスネークケースで記述されることが多いため、両者の命名規則の違いを適切にマッピングする必要があります。
例えば、JSONの"firstName"
をC#のFirstName
プロパティにマッピングする場合、System.Text.Json
ではJsonPropertyName
属性を使って明示的に指定できます。
using System.Text.Json.Serialization;
public class Person
{
[JsonPropertyName("firstName")]
public string FirstName { get; set; }
}
この属性を付けることで、JSONのfirstName
とC#のFirstName
が正しく対応し、デシリアライズやシリアライズ時に名前の不一致による問題を防げます。
また、JsonSerializerOptions
のPropertyNamingPolicy
にJsonNamingPolicy.CamelCase
を設定すると、C#のPascalCaseプロパティを自動的にcamelCaseに変換してくれます。
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
これにより、属性を付けずに命名規則の違いを吸収できますが、API仕様が複雑な場合や一部だけ異なる命名規則が混在する場合は属性での明示的指定が安全です。
Newtonsoft.Json
の場合は、JsonProperty
属性を使って同様のマッピングが可能です。
using Newtonsoft.Json;
public class Person
{
[JsonProperty("firstName")]
public string FirstName { get; set; }
}
命名規則の違いを正しく扱うことは、JSONデータの正確な読み書きに不可欠です。
null許容参照型とバリデーション
C# 8.0以降では、null許容参照型(Nullable Reference Types)が導入され、参照型の変数がnullを許容するかどうかを型システムで明示的に管理できます。
JSONデータをクラスに変換する際も、この機能を活用してnull安全な設計を行うことが重要です。
例えば、JSONのプロパティが必ず存在し、nullにならないことが保証されている場合は、非null型で定義します。
public class Person
{
public string Name { get; set; } = null!; // null許容しないが初期化は後で行う
}
一方、JSONのプロパティが省略可能またはnullを許容する場合は、nullable型にします。
public class Person
{
public string? Nickname { get; set; }
}
この設計により、コンパイラがnullチェックを支援し、null参照例外のリスクを減らせます。
さらに、バリデーションを組み合わせることで、JSONデータの整合性を保てます。
例えば、System.ComponentModel.DataAnnotations
の属性を使い、必須項目や文字列長の制限を指定できます。
using System.ComponentModel.DataAnnotations;
public class Person
{
[Required]
public string Name { get; set; } = null!;
[StringLength(100)]
public string? Nickname { get; set; }
}
デシリアライズ後にバリデーションを実行し、不正なデータを検出する運用が望ましいです。
record型とクラス型の使い分け
C# 9.0以降で導入されたrecord
型は、不変オブジェクトの表現に適しており、値の比較やコピーが簡単に行えます。
JSONデータを扱うモデルとしても利用されることが増えています。
public record Person(string Name, int Age);
record
型のメリットは以下の通りです。
- イミュータブル(不変)な設計がしやすい
- 値の等価性比較がデフォルトで実装されている
with
式による簡単なコピーが可能
ただし、record
型はデフォルトでコンストラクター引数を使うため、System.Text.Json
でデシリアライズする際は、パラメーター付きコンストラクターをサポートするバージョン(.NET 5以降)が必要です。
一方、従来のclass
型は可変オブジェクトとして設計されることが多く、プロパティのsetterを持つため、柔軟な変更が可能です。
使い分けのポイントは以下の通りです。
特徴 | record型 | class型 |
---|---|---|
不変性 | 高い | 低い(可変) |
等価性比較 | 値ベース | 参照ベース |
デシリアライズ対応 | .NET 5以降でパラメーター付きコンストラクター対応 | どのバージョンでも対応 |
変更の必要性 | 変更不可または限定的 | 柔軟に変更可能 |
不変オブジェクトとして設計したい場合や、値の比較が重要な場合はrecord
型を選び、柔軟な変更や後からのプロパティ設定が必要な場合はclass
型を選ぶと良いでしょう。
継承とポリモーフィズム対応
JSONデータに継承関係やポリモーフィズムが含まれる場合、C#のクラス設計もそれに対応する必要があります。
例えば、動物の基本クラスAnimal
を継承したDog
やCat
のようなケースです。
public abstract class Animal
{
public string Name { get; set; } = null!;
}
public class Dog : Animal
{
public bool IsGoodBoy { get; set; }
}
public class Cat : Animal
{
public int LivesLeft { get; set; }
}
このような場合、JSONのどの型に対応するかを判別するための識別子(Discriminator)が必要です。
JSONにtype
プロパティを設けて、どの派生クラスかを示す方法が一般的です。
{
"type": "dog",
"name": "Pochi",
"isGoodBoy": true
}
System.Text.Json
では、.NET 7以降でポリモーフィックシリアライズが強化されましたが、まだ限定的なため、カスタムコンバーターを実装して判別処理を行うことが多いです。
Newtonsoft.Json
では、JsonConverter
を使ってtype
プロパティを判別し、適切な派生クラスにデシリアライズすることが可能です。
[JsonConverter(typeof(AnimalConverter))]
public abstract class Animal { ... }
ポリモーフィズム対応は設計が複雑になるため、JSONの仕様を明確にし、識別子の設計やカスタムコンバーターの実装を慎重に行う必要があります。
コレクションと辞書のマッピング
JSONの配列やオブジェクトは、C#のコレクションや辞書にマッピングされます。
代表的なマッピング例は以下の通りです。
- JSON配列 →
List<T>
、T[]
- JSONオブジェクト(キー・バリューの集合) →
Dictionary<string, T>
例えば、JSON配列をList<Person>
にマッピングする例です。
[
{ "name": "penta", "age": 25 },
{ "name": "ponta", "age": 40 }
]
public class Person
{
public string Name { get; set; } = null!;
public int Age { get; set; }
}
public class Program
{
public static void Main()
{
string json = @"[
{ ""name"": ""penta"", ""age"": 25 },
{ ""name"": ""ponta"", ""age"": 40 }
]";
var people = JsonSerializer.Deserialize<List<Person>>(json);
}
}
辞書の場合は、JSONのキーが文字列である必要があります。
{
"penta": { "age": 25 },
"ponta": { "age": 40 }
}
var dict = JsonSerializer.Deserialize<Dictionary<string, Person>>(json);
注意点として、JSONのキーが数値や複雑な型の場合は、辞書のキー型をstring
以外にできないため、変換処理が必要になることがあります。
また、コレクションの型はIEnumerable<T>
やIList<T>
などインターフェースでも受け取れますが、デシリアライズ時には具体的な型(List<T>
など)を指定するほうが安全です。
Enumとビットフラグの扱い
JSONの値をC#のenum
型にマッピングする場合、文字列または数値で表現されることが多いです。
System.Text.Json
やNewtonsoft.Json
では、enum
のシリアライズ・デシリアライズを制御するための属性や設定があります。
public enum Status
{
Active,
Inactive,
Pending
}
JSONで文字列として表現されている場合は、JsonStringEnumConverter
を使うと便利です。
var options = new JsonSerializerOptions();
options.Converters.Add(new JsonStringEnumConverter());
var status = JsonSerializer.Deserialize<Status>("\"Active\"", options);
ビットフラグを使う[Flags]
属性付きのenum
もJSONで扱えますが、複数のフラグが文字列でカンマ区切りになっている場合は、カスタムコンバーターが必要になることがあります。
[Flags]
public enum Permissions
{
None = 0,
Read = 1,
Write = 2,
Execute = 4
}
JSONで"Read, Write"
のように表現されている場合、標準のコンバーターでは対応できないため、独自の変換ロジックを実装します。
既定値と既存プロパティの差異管理
JSONデータに含まれないプロパティや、既定値を持つプロパティの扱いは設計上重要です。
C#のクラスでプロパティに初期値を設定している場合、JSONにそのプロパティが存在しなければ初期値が使われます。
public class Person
{
public string Name { get; set; } = "Unknown";
public int Age { get; set; } = 0;
}
JSONに"age"
がなければAge
は0
のままになります。
System.Text.Json
のJsonSerializerOptions
でDefaultIgnoreCondition
を設定すると、既定値のプロパティをシリアライズから除外できます。
var options = new JsonSerializerOptions
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault
};
また、JSONに存在しないプロパティを無視したい場合は、IgnoreNullValues
やIgnoreReadOnlyProperties
などの設定も活用します。
既定値とJSONの差異を管理することで、不要なデータの送受信を減らし、APIの互換性を保てます。
循環参照を含むモデルの構築
JSONデータやC#のクラスで循環参照が発生する場合、シリアライズ・デシリアライズ時に問題が起きやすいです。
例えば、親子関係のモデルで親が子を参照し、子が親を参照するケースです。
public class Parent
{
public string Name { get; set; } = null!;
public List<Child> Children { get; set; } = new();
}
public class Child
{
public string Name { get; set; } = null!;
public Parent Parent { get; set; } = null!;
}
System.Text.Json
はデフォルトで循環参照を検出すると例外を投げます。
これを回避するには、ReferenceHandler.Preserve
を設定します。
var options = new JsonSerializerOptions
{
ReferenceHandler = ReferenceHandler.Preserve,
WriteIndented = true
};
この設定により、循環参照を特殊なメタデータで表現し、シリアライズ・デシリアライズが可能になります。
Newtonsoft.Json
では、PreserveReferencesHandling
を使って同様の対応ができます。
var settings = new JsonSerializerSettings
{
PreserveReferencesHandling = PreserveReferencesHandling.Objects
};
ただし、循環参照のあるモデルは設計が複雑になるため、可能な限り循環を避けるか、DTO(データ転送オブジェクト)を分けて循環を解消する方法も検討してください。
System.Text.Jsonによる変換
基本的なDeserializeの書式
System.Text.Json
は.NET Core 3.0以降で標準搭載されたJSON処理ライブラリで、高速かつ軽量なシリアライズ・デシリアライズを実現しています。
JSON文字列をC#のクラスに変換する基本的な書式は以下の通りです。
using System;
using System.Text.Json;
public class Person
{
public string Name { get; set; } = null!;
public int Age { get; set; }
}
public class Program
{
public static void Main()
{
string jsonString = @"{ ""Name"": ""penta"", ""Age"": 25 }";
Person person = JsonSerializer.Deserialize<Person>(jsonString)!;
Console.WriteLine($"Name: {person.Name}, Age: {person.Age}");
}
}
Name: penta, Age: 25
JsonSerializer.Deserialize<T>()
メソッドにJSON文字列と変換先の型を指定するだけで、簡単にデシリアライズできます。
null許容参照型を有効にしている場合は、戻り値がnullになる可能性があるため、!
演算子で非nullを明示することが多いです。
JsonSerializerOptionsでできること
JsonSerializerOptions
は、シリアライズ・デシリアライズの挙動を細かく制御できる設定クラスです。
主な設定項目を紹介します。
PropertyNameCaseInsensitive
JSONのプロパティ名とC#のプロパティ名の大文字小文字が異なる場合、デフォルトではマッチしません。
PropertyNameCaseInsensitive
をtrue
に設定すると、大文字小文字を区別せずにマッピングできます。
var options = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
};
string json = @"{ ""name"": ""penta"", ""age"": 25 }";
Person person = JsonSerializer.Deserialize<Person>(json, options)!;
Console.WriteLine($"Name: {person.Name}, Age: {person.Age}");
Name: penta, Age: 25
この設定はAPIからのJSONがcamelCaseで送られてくる場合などに便利です。
DefaultIgnoreCondition
シリアライズ時に、既定値やnullのプロパティを除外したい場合に使います。
例えば、WhenWritingNull
を指定すると、nullのプロパティはJSONに含まれません。
var options = new JsonSerializerOptions
{
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
};
WhenWritingDefault
を指定すると、既定値(例えばintの0やboolのfalse)も除外されます。
Encoderとセキュリティ設定
JSONの文字列に含まれる特殊文字をエスケープするためのエンコーダーを指定できます。
デフォルトはJavaScriptEncoder.Default
ですが、HTMLエスケープを強化したい場合はJavaScriptEncoder.UnsafeRelaxedJsonEscaping
などを使います。
var options = new JsonSerializerOptions
{
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
これにより、例えば<
や>
などの文字がエスケープされずに出力され、HTML埋め込み時の安全性や可読性を調整できます。
Attributesの活用
System.Text.Json
では、属性を使ってクラスのプロパティごとにシリアライズ・デシリアライズの挙動を制御できます。
JsonPropertyName
JSONのプロパティ名とC#のプロパティ名が異なる場合に使います。
using System.Text.Json.Serialization;
public class Person
{
[JsonPropertyName("first_name")]
public string FirstName { get; set; } = null!;
}
この例では、JSONのfirst_name
がFirstName
プロパティにマッピングされます。
JsonIgnore
特定のプロパティをシリアライズ・デシリアライズから除外したい場合に使います。
public class Person
{
public string Name { get; set; } = null!;
[JsonIgnore]
public int InternalId { get; set; }
}
InternalId
はJSONに含まれません。
JsonInclude
プライベートなsetterやフィールドをシリアライズ・デシリアライズ対象にしたい場合に使います。
public class Person
{
public string Name { get; private set; } = null!;
[JsonInclude]
public int Age { get; private set; }
}
通常はpublicなsetterが必要ですが、JsonInclude
を付けるとprivate setterでも処理されます。
JsonConverter
特定の型に対してカスタムの変換処理を指定できます。
例えば、日付のフォーマットや独自の列挙型変換などに使います。
[JsonConverter(typeof(CustomDateTimeConverter))]
public DateTime Date { get; set; }
カスタムコンバーター
日付書式変換
JSONの日付フォーマットが標準のISO 8601形式でない場合、カスタムコンバーターで対応します。
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
public class CustomDateTimeConverter : JsonConverter<DateTime>
{
private const string Format = "yyyyMMdd";
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
string? value = reader.GetString();
return DateTime.ParseExact(value!, Format, null);
}
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString(Format));
}
}
このコンバーターをプロパティに適用すると、"20230601"
のような日付文字列をDateTime
に変換できます。
数値→列挙型
JSONで数値として表現される列挙型を文字列に変換したい場合や、その逆もカスタムコンバーターで対応可能です。
public class IntEnumConverter<T> : JsonConverter<T> where T : Enum
{
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
int intValue = reader.GetInt32();
return (T)Enum.ToObject(typeof(T), intValue);
}
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
writer.WriteNumberValue(Convert.ToInt32(value));
}
}
ポリモーフィック型
継承階層のあるクラスをJSONに変換する場合、どの派生クラスかを判別して適切に処理するためにカスタムコンバーターを実装します。
System.Text.Json
は標準でのポリモーフィック対応が限定的なため、手動でRead
メソッド内でtype
プロパティを判別し、適切な型にデシリアライズします。
ソースジェネレーターの導入で高速化
生成対象の指定
System.Text.Json
のソースジェネレーターを使うと、ビルド時にシリアライズ・デシリアライズコードを自動生成し、ランタイムのリフレクションを排除して高速化できます。
対象の型をJsonSerializable
属性で指定します。
[JsonSerializable(typeof(Person))]
public partial class PersonJsonContext : JsonSerializerContext
{
}
この設定により、Person
型の変換コードがビルド時に生成されます。
AOTとILサイズの影響
ソースジェネレーターはAhead-Of-Time(AOT)コンパイル環境で特に効果を発揮し、リフレクションを使わないためILサイズの増加を抑えられます。
ただし、生成コードが増えるため、プロジェクトのビルド時間や出力ファイルサイズに影響が出ることもあります。
適切に生成対象を絞ることが重要です。
ストリーミングAPI
Utf8JsonReaderの活用
Utf8JsonReader
は低レベルのJSON読み取りAPIで、部分的にJSONを読み込みながら処理できます。
大きなJSONファイルやストリームからの逐次処理に適しています。
using System;
using System.Buffers;
using System.Text.Json;
public class Program
{
public static void Main()
{
string json = @"{ ""Name"": ""penta"", ""Age"": 25 }";
var bytes = System.Text.Encoding.UTF8.GetBytes(json);
var reader = new Utf8JsonReader(bytes);
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.PropertyName)
{
string propertyName = reader.GetString()!;
reader.Read();
string value = reader.GetString()!;
Console.WriteLine($"{propertyName}: {value}");
}
}
}
}
Name: penta
Age: 25
部分読み込みのパターン
Utf8JsonReader
を使うと、JSONの一部だけを読み込んで処理したり、巨大な配列を分割して処理したりできます。
これによりメモリ使用量を抑えつつ効率的にデータを扱えます。
エラーハンドリング
JsonExceptionの取り扱い
JsonSerializer.Deserialize
やUtf8JsonReader
は不正なJSONや型不一致があるとJsonException
を投げます。
例外処理でキャッチし、ログ出力やリトライ処理を行うことが一般的です。
try
{
var person = JsonSerializer.Deserialize<Person>(jsonString);
}
catch (JsonException ex)
{
Console.WriteLine($"JSONの解析に失敗しました: {ex.Message}");
}
不正データの回復戦略
不正なJSONデータを完全に拒否するのではなく、一部のプロパティだけを無視して処理を続行したい場合は、JsonSerializerOptions
のIgnoreNullValues
やDefaultIgnoreCondition
を活用したり、カスタムコンバーターで例外を吸収する方法があります。
また、Utf8JsonReader
を使って手動でパースし、問題のある部分だけスキップする実装も可能です。
これにより、部分的に不正なデータが混在していても、可能な限り正常なデータを取得できます。
Newtonsoft.Jsonによる変換
基本的なDeserializeObjectの書式
Newtonsoft.Json
(通称Json.NET)は、.NETで広く使われている高機能なJSONライブラリです。
JSON文字列をC#のクラスに変換する基本的な書式は以下の通りです。
using System;
using Newtonsoft.Json;
public class Person
{
public string Name { get; set; } = null!;
public int Age { get; set; }
}
public class Program
{
public static void Main()
{
string jsonString = @"{ ""Name"": ""penta"", ""Age"": 25 }";
Person person = JsonConvert.DeserializeObject<Person>(jsonString)!;
Console.WriteLine($"Name: {person.Name}, Age: {person.Age}");
}
}
Name: penta, Age: 25
JsonConvert.DeserializeObject<T>()
メソッドにJSON文字列と変換先の型を指定するだけで、簡単にデシリアライズできます。
null許容参照型を有効にしている場合は、戻り値がnullになる可能性があるため、!
演算子で非nullを明示することが多いです。
設定オブジェクトJsonSerializerSettings
JsonSerializerSettings
は、シリアライズ・デシリアライズの挙動を細かく制御できる設定クラスです。
主な設定項目を紹介します。
MissingMemberHandling
JSONに存在しないプロパティがC#のクラスにある場合の挙動を制御します。
Ignore
(デフォルト):無視して処理を続行Error
:例外をスロー
var settings = new JsonSerializerSettings
{
MissingMemberHandling = MissingMemberHandling.Error
};
この設定により、JSONに存在しないプロパティがあれば例外が発生し、データの不整合を検出できます。
NullValueHandling
null値のプロパティの扱いを制御します。
Include
(デフォルト):null値もシリアライズ・デシリアライズ対象Ignore
:null値のプロパティを無視
var settings = new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Ignore
};
null値を送信したくないAPIや、デフォルト値を使いたい場合に有効です。
DefaultValueHandling
既定値のプロパティの扱いを制御します。
Include
(デフォルト):既定値もシリアライズ・デシリアライズ対象Ignore
:既定値のプロパティを無視
var settings = new JsonSerializerSettings
{
DefaultValueHandling = DefaultValueHandling.Ignore
};
既定値の送信を省略して通信量を削減したい場合に使います。
TypeNameHandling
JSONに型情報を埋め込むかどうかを制御します。
ポリモーフィズム対応に使われます。
None
(デフォルト):型情報なしObjects
:オブジェクトに型情報を付与All
:すべての値に型情報を付与
var settings = new JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.Objects
};
型情報を含めることで、派生クラスのデシリアライズが可能になりますが、セキュリティリスクもあるため注意が必要です。
Attributesの活用
Newtonsoft.Json
は属性を使ってプロパティごとの挙動を細かく制御できます。
JsonProperty
JSONのプロパティ名とC#のプロパティ名が異なる場合に使います。
using Newtonsoft.Json;
public class Person
{
[JsonProperty("first_name")]
public string FirstName { get; set; } = null!;
}
JsonRequired
必須プロパティとしてマークし、JSONに存在しない場合は例外をスローします。
public class Person
{
[JsonRequired]
public string Name { get; set; } = null!;
}
JsonConverter
特定の型に対してカスタム変換処理を指定します。
[JsonConverter(typeof(CustomDateTimeConverter))]
public DateTime Date { get; set; }
JsonIgnore
シリアライズ・デシリアライズから除外したいプロパティに付けます。
public class Person
{
[JsonIgnore]
public int InternalId { get; set; }
}
カスタムコンバーター
カスタムDateTimeフォーマット
標準のISO 8601形式以外の日付フォーマットを扱う場合、カスタムコンバーターを実装します。
using System;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using System.Globalization;
public class CustomDateTimeConverter : DateTimeConverterBase
{
private const string Format = "yyyyMMdd";
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{
if (value is DateTime dt)
{
writer.WriteValue(dt.ToString(Format));
}
else
{
writer.WriteNull();
}
}
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
string? s = reader.Value?.ToString();
if (DateTime.TryParseExact(s, Format, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dt))
{
return dt;
}
return null;
}
}
Polymorphic変換
TypeNameHandling
を使うか、カスタムJsonConverter
を実装して、派生クラスの判別と変換を行います。
[JsonConverter(typeof(AnimalConverter))]
public abstract class Animal { ... }
カスタムコンバーター内で$type
プロパティを読み取り、適切な派生クラスにデシリアライズします。
JObjectとdynamicを使うケース
部分更新Patchパターン
JObject
を使うと、JSONの一部だけを動的に操作できます。
APIのPATCHリクエストなど、部分的な更新に便利です。
JObject patch = JObject.Parse(@"{ ""Name"": ""newName"" }");
existingObject.Merge(patch);
スキーマレスデータの操作
スキーマが不明なJSONや可変構造のデータを扱う場合、dynamic
型やJObject
で柔軟にアクセスできます。
dynamic obj = JsonConvert.DeserializeObject<dynamic>(jsonString);
Console.WriteLine(obj.name);
ただし、型安全性がないためランタイムエラーに注意が必要です。
パフォーマンス最適化
JsonLoadSettingsとメモリ削減
JObject.Load
などでJSONを読み込む際、JsonLoadSettings
を使ってコメントや重複プロパティの処理を制御し、メモリ使用量を抑えられます。
var settings = new JsonLoadSettings
{
CommentHandling = CommentHandling.Ignore,
DuplicatePropertyNameHandling = DuplicatePropertyNameHandling.Ignore
};
JObject obj = JObject.Load(reader, settings);
シリアライズ・デシリアライズ分割読み込み
大きなJSONを一度に読み込まず、JsonTextReader
を使ってストリームから部分的に読み込むことでメモリ効率を改善できます。
using (var reader = new JsonTextReader(new StreamReader(stream)))
{
while (reader.Read())
{
// 必要なトークンだけ処理
}
}
エラー処理
ErrorContextイベント
JsonSerializerSettings
のError
イベントを利用して、デシリアライズ時のエラーをキャッチし、ログ出力や無視処理が可能です。
var settings = new JsonSerializerSettings();
settings.Error += (sender, args) =>
{
Console.WriteLine($"Error: {args.ErrorContext.Error.Message}");
args.ErrorContext.Handled = true; // エラーを無視して続行
};
再試行と部分的無視
エラー発生時に再試行や部分的に無視して処理を続けることで、堅牢なJSON処理が実現できます。
特に外部APIの不正データに対して有効です。
System.Text.JsonとNewtonsoft.Jsonの比較
機能と拡張性
System.Text.Json
は.NET Core 3.0以降に標準搭載されたJSONライブラリで、軽量かつ高速な処理を特徴としています。
基本的なシリアライズ・デシリアライズ機能は十分に備えていますが、拡張性や高度な機能面ではNewtonsoft.Json
に劣る部分があります。
一方、Newtonsoft.Json
は長年にわたり多くの機能拡張が積み重ねられており、以下のような特徴があります。
- 豊富な属性サポート
プロパティの細かい制御やカスタム変換、条件付きシリアライズなど多彩な属性が利用可能です。
- 柔軟なカスタムコンバーター
複雑な型変換やポリモーフィズム対応が容易に実装できます。
- LINQ to JSON
JObject
やJArray
を使った動的かつ柔軟なJSON操作が可能です。
- JSON Pathサポート
JSON内の特定ノードを簡単に抽出できます。
- XMLとの相互変換
JSONとXMLの変換機能が組み込まれています。
System.Text.Json
はこれらの高度な機能の一部を順次追加していますが、現時点ではNewtonsoft.Json
の機能の豊富さには及びません。
特に動的JSON操作や複雑なポリモーフィズム対応が必要な場合はNewtonsoft.Json
が優位です。
パフォーマンスベンチマーク
System.Text.Json
はパフォーマンス面で非常に優れており、特に以下の点で高速化が図られています。
- リフレクションの使用を最小化
ソースジェネレーターの導入により、ビルド時にシリアライズコードを生成し、ランタイムのリフレクションを排除可能です。
- 低メモリ消費
ストリーミングAPIやUtf8JsonReader
を活用し、メモリ効率の良い処理が可能です。
- ネイティブUTF-8対応
UTF-8バイト列を直接処理し、エンコード・デコードのオーバーヘッドを削減しています。
一方、Newtonsoft.Json
は機能の豊富さゆえにリフレクションを多用し、メモリ消費や処理速度でSystem.Text.Json
に劣る傾向があります。
ただし、複雑な変換や動的処理が必要な場合は、Newtonsoft.Json
の柔軟性がパフォーマンス低下を補うこともあります。
ベンチマーク結果は環境やシナリオによって異なりますが、単純なシリアライズ・デシリアライズ処理ではSystem.Text.Json
が数倍高速であるケースが多いです。
対応プラットフォームとライブラリサイズ
System.Text.Json
は.NET Core 3.0以降および.NET 5/6/7で標準搭載されており、追加の外部依存なしに利用可能です。
ライブラリサイズも小さく、クロスプラットフォーム開発や軽量な環境に適しています。
Newtonsoft.Json
は.NET Framework時代からの実績があり、.NET Framework、.NET Core、.NET 5以降、Mono、Xamarin、Unityなど幅広いプラットフォームで利用可能です。
ただし、NuGetパッケージとして追加する必要があり、ライブラリサイズはSystem.Text.Json
より大きめです。
軽量で標準搭載のSystem.Text.Json
は、特に新規プロジェクトやパフォーマンス重視の環境に向いています。
既存の.NET Frameworkプロジェクトや特殊なプラットフォームではNewtonsoft.Json
が依然として主流です。
非対応機能と回避策
System.Text.Json
はまだ発展途上のため、以下のような機能が不足しています。
- 高度なポリモーフィズム対応
標準では派生クラスの自動判別が限定的で、カスタムコンバーターの実装が必要です。
- 動的JSON操作
JObject
やJArray
のような動的かつ柔軟なJSON操作機能はありません。
- JSON Pathサポート
JSON内の特定ノードを簡単に抽出する機能は未実装です。
- XMLとの相互変換
JSONとXMLの変換機能は提供されていません。
- 一部の属性や設定の不足
Newtonsoft.Json
のような細かい属性制御が限定的です。
これらの不足機能は、以下のような回避策で対応可能です。
- ポリモーフィズムはカスタム
JsonConverter
を実装して判別処理を行う - 動的操作が必要な場合は
Newtonsoft.Json
を併用する - JSON Pathが必要な場合は
Newtonsoft.Json
のJToken.SelectToken
を利用する - XML変換は別ライブラリや手動実装で補う
移行の判断基準
既存プロジェクトでNewtonsoft.Json
からSystem.Text.Json
への移行を検討する際は、以下のポイントを考慮してください。
- 機能要件の適合
必要な機能がSystem.Text.Json
でサポートされているか。
高度なポリモーフィズムや動的操作が多い場合は移行が難しいです。
- パフォーマンス要件
処理速度やメモリ使用量の改善が重要であれば、System.Text.Json
の導入を検討します。
- プラットフォーム要件
.NET Core 3.0以降や.NET 5/6/7を使用しているか。
古い.NET FrameworkではNewtonsoft.Json
が必須です。
- メンテナンス性と将来性
System.Text.Json
はMicrosoft公式の標準ライブラリであり、今後の機能追加や最適化が期待できます。
- 既存コードの影響範囲
移行に伴うコード修正量やテストコストを評価し、リスクを最小化する計画を立てます。
- 混在運用の可能性
機能不足の部分だけNewtonsoft.Json
を残し、基本はSystem.Text.Json
を使うハイブリッド運用も選択肢です。
これらを踏まえ、プロジェクトの要件や開発体制に最適なJSONライブラリを選択してください。
実装パターン別Tips
小規模APIクライアント
JsonSerializerと匿名型活用例
小規模なAPIクライアントでは、すべてのレスポンスに対して専用のモデルクラスを作成するのが過剰になる場合があります。
そんなときは、System.Text.Json
のJsonSerializer
と匿名型を組み合わせて簡潔にJSONを扱う方法が便利です。
using System;
using System.Text.Json;
public class Program
{
public static void Main()
{
string json = @"{ ""name"": ""penta"", ""age"": 25 }";
var template = new { name = "", age = 0 };
var result = JsonSerializer.Deserialize(json, template.GetType());
Console.WriteLine($"Name: {result.GetType().GetProperty("name")!.GetValue(result)}, Age: {result.GetType().GetProperty("age")!.GetValue(result)}");
}
}
Name: penta, Age: 25
この方法は、簡単なデータ構造の取得に向いていますが、型安全性は低く、複雑な処理や再利用性が求められる場合は専用クラスを作成したほうが良いです。
ビッグデータ処理
ストリーミングとメモリ管理
大量のJSONデータを一括で読み込むとメモリ不足やパフォーマンス低下の原因になります。
System.Text.Json
のUtf8JsonReader
を使ったストリーミング処理で、メモリ消費を抑えつつ効率的にデータを処理できます。
using System;
using System.Buffers;
using System.Text;
using System.Text.Json;
public class Program
{
public static void Main()
{
string json = @"[{""name"":""penta"",""age"":25},{""name"":""ponta"",""age"":40}]";
byte[] jsonData = Encoding.UTF8.GetBytes(json);
var reader = new Utf8JsonReader(jsonData);
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.StartObject)
{
string? name = null;
int age = 0;
while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)
{
if (reader.TokenType == JsonTokenType.PropertyName)
{
string propertyName = reader.GetString()!;
reader.Read();
if (propertyName == "name")
{
name = reader.GetString();
}
else if (propertyName == "age")
{
age = reader.GetInt32();
}
}
}
Console.WriteLine($"Name: {name}, Age: {age}");
}
}
}
}
Name: penta, Age: 25
Name: ponta, Age: 40
このようにストリーミングで部分的に読み込むことで、大量データの処理が可能になります。
クロスプラットフォームアプリ
XamarinとUnityの制約
XamarinやUnityなどのクロスプラットフォーム環境では、System.Text.Json
の一部機能が制限される場合があります。
特にUnityは.NETのサブセットを使っているため、System.Text.Json
の完全なサポートがありません。
- Xamarin
基本的にSystem.Text.Json
は利用可能ですが、古いバージョンのXamarinでは互換性に注意が必要です。
Newtonsoft.Json
は広く使われており、安定した動作が期待できます。
- Unity
UnityのMonoランタイムはSystem.Text.Json
を完全にサポートしていません。
Newtonsoft.Json
を利用するか、Unity向けに最適化されたJSONライブラリを使うことが多いです。
クロスプラットフォーム開発では、ターゲット環境の制約を考慮してJSONライブラリを選択し、必要に応じて条件付きコンパイルやラッパークラスで対応すると良いでしょう。
ASP.NET Core Web API
モデルバインディングとコントローラー設計
ASP.NET Core Web APIでは、JSONデータの受け取りと返却が頻繁に行われます。
System.Text.Json
がデフォルトのシリアライザーですが、モデルバインディングの設計に注意が必要です。
- モデルクラスの設計
入力モデル(DTO)と出力モデルを分けることで、APIの責務を明確にし、セキュリティや拡張性を高められます。
- 属性の活用
[JsonPropertyName]
や[JsonIgnore]
を使い、API仕様に合わせてプロパティの名前やシリアライズ対象を制御します。
- バリデーション
DataAnnotations
属性を使い、モデルのバリデーションを行い、不正なデータの受け入れを防ぎます。
- カスタムコンバーターの利用
日付や列挙型の特殊なフォーマットに対応するため、カスタムコンバーターを登録します。
- コントローラー設計
アクションメソッドの引数にモデルを指定し、[FromBody]
属性でJSONをバインドします。
[HttpPost]
public IActionResult CreatePerson([FromBody] PersonCreateDto dto)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
// 処理
return Ok();
}
サードパーティAPI連携
レスポンス変更への耐性強化
外部APIのレスポンスは予告なく変更されることが多いため、耐性を持たせる設計が重要です。
- 余分なプロパティの無視
System.Text.Json
のJsonSerializerOptions.PropertyNameCaseInsensitive
やNewtonsoft.Json
のMissingMemberHandling.Ignore
を設定し、未知のプロパティがあっても例外を防ぎます。
- nullable型の活用
省略可能なプロパティはnullableにして、欠損データに対応します。
- 部分的なモデル化
必要なプロパティだけをモデルに定義し、不要な部分は無視します。
- カスタムコンバーターで柔軟対応
変化しやすいデータ型やフォーマットに対してはカスタムコンバーターを用意し、変換ロジックを集中管理します。
- 動的型の併用
どうしても不確定な部分はJObject
やdynamic
で受け取り、必要に応じて解析します。
ロギングとモニタリング
敏感情報マスキング
ログにJSONデータを出力する際、パスワードやトークンなどの敏感情報をマスクすることが重要です。
- カスタムシリアライザー
シリアライズ前に敏感情報を置換または削除する処理を挟みます。
- 属性による制御
[JsonIgnore]
やカスタム属性を使い、ログ用のDTOを別途用意する方法もあります。
- ログフィルター
ログ出力時に特定のキーを検出してマスクする仕組みを導入します。
エラー情報の最小化
エラー発生時のログは詳細すぎるとセキュリティリスクやノイズになります。
- 例外メッセージの制御
内部情報を含まない簡潔なメッセージにします。
- スタックトレースの制限
本番環境ではスタックトレースを省略し、必要に応じて開発環境でのみ詳細を出力。
- ログレベルの適切な設定
エラーの重要度に応じてログレベルを使い分け、過剰なログを防ぎます。
これらの工夫により、JSONデータを扱う際のトラブルを減らし、保守性とセキュリティを高められます。
よくある落とし穴と対処法
大文字小文字不一致
JSONのプロパティ名は一般的にcamelCase(先頭小文字)で記述されることが多い一方、C#のプロパティ名はPascalCase(先頭大文字)が標準です。
この命名規則の違いによって、デシリアライズ時にプロパティが正しくマッピングされず、値が設定されない問題がよく発生します。
例えば、JSONに"firstName"
というプロパティがあるのに、C#のクラスでFirstName
と定義している場合、System.Text.Json
のデフォルト設定ではマッチしません。
対処法
System.Text.Json
の場合は、JsonSerializerOptions
のPropertyNameCaseInsensitive
をtrue
に設定して大文字小文字を無視します
var options = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
};
- プロパティごとに
[JsonPropertyName("jsonプロパティ名")]
属性を付けて明示的にマッピングします
[JsonPropertyName("firstName")]
public string FirstName { get; set; }
Newtonsoft.Json
を使う場合は、[JsonProperty("firstName")]
属性を使います
[JsonProperty("firstName")]
public string FirstName { get; set; }
これらの対策により、命名規則の違いによるマッピングミスを防げます。
循環参照例外
C#のオブジェクトモデルで親子関係や双方向参照がある場合、シリアライズ時に循環参照が発生し、例外が投げられることがあります。
例えば、親オブジェクトが子オブジェクトを参照し、子オブジェクトが親オブジェクトを参照しているケースです。
対処法
System.Text.Json
では、JsonSerializerOptions
のReferenceHandler
にReferenceHandler.Preserve
を設定します
var options = new JsonSerializerOptions
{
ReferenceHandler = ReferenceHandler.Preserve,
WriteIndented = true
};
Newtonsoft.Json
では、JsonSerializerSettings
のPreserveReferencesHandling
をPreserveReferencesHandling.Objects
に設定します
var settings = new JsonSerializerSettings
{
PreserveReferencesHandling = PreserveReferencesHandling.Objects
};
- 循環参照を避けるために、DTOを分けて片方向の参照にする設計も有効です
[JsonIgnore]
属性を使って片方の参照をシリアライズ対象から除外する方法もあります
余分な末尾カンマ
JSONの配列やオブジェクトの最後の要素の後に余分なカンマがあると、多くのJSONパーサーでエラーになります。
例えば、以下のようなJSONは不正です。
{
"name": "penta",
"age": 25,
}
対処法
- JSON生成元のコードやツールを修正して、末尾カンマを付けないようにします
Newtonsoft.Json
のJsonLoadSettings
でCommentHandling
やAllowTrailingCommas
を設定し、末尾カンマを許容します
var settings = new JsonLoadSettings
{
CommentHandling = CommentHandling.Ignore,
AllowTrailingCommas = true
};
System.Text.Json
では、JsonReaderOptions
のAllowTrailingCommas
をtrue
に設定して許容可能です
var options = new JsonReaderOptions
{
AllowTrailingCommas = true
};
ただし、末尾カンマはJSONの正式仕様では許容されていないため、可能な限り生成元で修正することが望ましいです。
クォートエスケープの誤り
JSON文字列内でダブルクォートやバックスラッシュなどの特殊文字を正しくエスケープしないと、パースエラーやデータ破損が発生します。
例えば、文字列内の"
は\"
とエスケープする必要があります。
対処法
- JSON生成時は必ず信頼できるライブラリやAPIを使い、手動で文字列を組み立てない
- 文字列を直接JSONに埋め込む場合は、エスケープ処理を正しく行います
System.Text.Json
やNewtonsoft.Json
のシリアライザーを使うと自動的にエスケープされるため、手動処理は避けます- JSONのバリデーションツールやオンラインパーサーで事前にチェックします
可変スキーマへの追従
APIのレスポンスや外部データのJSONスキーマが頻繁に変わる場合、固定のC#クラスで対応するとデシリアライズエラーやデータ欠損が起きやすくなります。
対処法
- 必要なプロパティだけをモデルに定義し、余分なプロパティは無視する設定を使います
System.Text.Json
ではJsonSerializerOptions
のIgnoreUnknownProperties
をtrue
に設定
var options = new JsonSerializerOptions
{
IgnoreUnknownProperties = true
};
Newtonsoft.Json
ではMissingMemberHandling
をIgnore
に設定- 動的型
dynamic
やJObject
を使い、スキーマレスにデータを扱います - JSON Schemaを利用してスキーマ検証を行い、変更点を検知します
- バージョニングやAPI仕様の管理を徹底し、変更に備えた設計を行います
これらの対策により、可変スキーマのJSONデータに柔軟に対応できます。
将来拡張アイデア
JSON Schemaによるバリデーション
JSON Schemaは、JSONデータの構造や型、制約を定義するための標準仕様です。
C#のクラスに変換したJSONデータに対して、JSON Schemaを用いたバリデーションを組み込むことで、データの整合性をより厳密にチェックできます。
具体的には、APIから受け取ったJSONをデシリアライズした後、JSON Schemaに基づいて検証を行い、不正なデータを早期に検出します。
これにより、バリデーションロジックをC#の属性やコードに分散させることなく、一元管理が可能です。
.NET環境では、NJsonSchema
やNewtonsoft.Json.Schema
などのライブラリがJSON Schemaの検証をサポートしています。
例えば、NJsonSchema
を使ったバリデーションの例は以下の通りです。
using NJsonSchema;
using NJsonSchema.Validation;
using System.Threading.Tasks;
public async Task ValidateJsonAsync(string json, string schemaJson)
{
var schema = await JsonSchema.FromJsonAsync(schemaJson);
var errors = schema.Validate(json);
if (errors.Count > 0)
{
foreach (var error in errors)
{
Console.WriteLine($"Error: {error.Path} - {error.Kind}");
}
throw new Exception("JSON validation failed.");
}
}
このようにJSON Schemaを活用すると、API仕様の変更に強く、堅牢なデータ処理が実現できます。
YAMLやXMLとの相互変換
JSON以外のデータフォーマットとしてYAMLやXMLも広く使われています。
将来的にこれらのフォーマットとの相互変換をサポートすることで、より柔軟なデータ連携が可能になります。
- YAMLとの相互変換
YAMLは人間に読みやすいフォーマットで、設定ファイルやドキュメントで多用されます。
C#ではYamlDotNet
ライブラリを使ってYAMLの読み書きが可能です。
JSONとYAMLは構造が似ているため、JSONを一旦C#のクラスにデシリアライズし、そこからYAMLにシリアライズする形で相互変換できます。
- XMLとの相互変換
XMLは古くから使われているマークアップ言語で、多くのレガシーシステムで利用されています。
System.Xml.Serialization.XmlSerializer
を使ってXMLとC#クラスの変換が可能です。
JSONとXMLの相互変換は、一旦C#のクラスに変換してから別フォーマットにシリアライズする方法が一般的です。
これらの相互変換を組み合わせることで、異なるシステム間でのデータ連携やマイグレーションがスムーズになります。
MessagePackやProtocol Buffersへの切り替え
JSONは可読性に優れていますが、データサイズが大きく、パース速度や帯域幅の面で制約がある場合があります。
そこで、バイナリ形式のシリアライズフォーマットであるMessagePackやProtocol Buffers(Protobuf)への切り替えが検討されます。
- MessagePack
MessagePackは高速かつコンパクトなバイナリフォーマットで、JSONと互換性が高いのが特徴です。
C#ではMessagePack-CSharp
ライブラリがあり、属性を付けるだけで簡単に利用できます。
[MessagePackObject]
public class Person
{
[Key(0)]
public string Name { get; set; } = null!;
[Key(1)]
public int Age { get; set; }
}
MessagePackはJSONよりもデータサイズが小さく、ネットワーク通信やストレージの効率化に寄与します。
- Protocol Buffers
Googleが開発したProtobufは、スキーマベースのバイナリシリアライズフォーマットで、高速かつ小容量のデータ交換が可能です。
C#ではGoogle.Protobuf
ライブラリを使い、.proto
ファイルからクラスを自動生成します。
Protobufはスキーマ管理が厳格で、バージョニングや後方互換性の管理に優れています。
大規模システムやマイクロサービス間通信で多用されています。
切り替えのポイント
- パフォーマンスや帯域幅が重要な場合はバイナリフォーマットを検討する
- 既存のJSONベースのAPIやシステムとの互換性を考慮し、段階的に移行する
- スキーマ管理やツールチェーンの整備が必要になるため、開発体制を整える
これらのフォーマットへの切り替えは、将来的なシステム拡張やパフォーマンス改善に有効な選択肢です。
まとめ
C#でJSONデータをクラスに変換する際は、System.Text.Json
とNewtonsoft.Json
の特徴や用途を理解し、プロジェクトに最適な方法を選ぶことが重要です。
Visual Studioの自動生成やオンラインツール、ソースジェネレーターを活用すると効率的にモデルを作成できます。
命名規則の違いや循環参照、可変スキーマなどの落とし穴にも注意が必要です。
将来的にはJSON SchemaによるバリデーションやYAML・XMLとの相互変換、さらにはMessagePackやProtocol Buffersへの切り替えも視野に入れ、柔軟で堅牢なデータ処理を目指しましょう。