【C#】DataSetをJSONへ高速変換するSerializeテクニックと逆変換手順
C#ではDataSetをJSONへ変換する際、System.Text.Json
やNewtonsoft.Json
でDataTable
あるいはDataSet全体をSerialize
できます。
テーブル名がキーとなり各行が配列要素です。
nullや型、日付のフォーマットを確認し列名を安定させることで双方向変換がスムーズに進みます。
DataSetとJSON変換の基本
DataSetとDataTableの相互関係
C#のDataSet
は、複数のDataTable
をまとめて管理できるデータ構造です。
DataTable
は行と列で構成される表形式のデータを表し、DataSet
はこれらのテーブルを複数持つことができます。
DataSet
は単なるテーブルの集合体ではなく、テーブル間のリレーション(親子関係)や制約も管理できるため、複雑なデータ構造を扱う際に便利です。
具体的には、DataSet
は以下のような役割を持っています。
- 複数の
DataTable
を格納し、一つのまとまりとして扱う - テーブル間のリレーションを定義し、親子関係を管理する
- データの整合性を保つための制約(主キー、外部キーなど)を設定できる
- データの変更履歴を追跡し、更新や削除の管理が可能
一方、DataTable
は単一の表を表現し、列DataColumn
と行DataRow
で構成されます。
DataTable
はデータベースのテーブルに似た構造で、列ごとに型が定義されているため、型安全にデータを扱えます。
DataSet
とDataTable
の関係は、以下のようにイメージできます。
構造体 | 役割 |
---|---|
DataSet | 複数のDataTableをまとめ、テーブル間の関係や制約を管理するコンテナ |
DataTable | 行と列で構成される単一の表形式データ |
JSONに変換する際は、DataSet
の中の各DataTable
を個別にJSON配列として表現し、それらをまとめて1つのJSONオブジェクトにすることが多いです。
これにより、元のテーブル構造を保ったままデータを表現できます。
JSONフォーマットの利点
JSON(JavaScript Object Notation)は、軽量で人間にも読みやすいテキスト形式のデータ交換フォーマットです。
C#のDataSet
をJSONに変換することには多くの利点があります。
- 言語やプラットフォームの互換性が高い
JSONは多くのプログラミング言語でサポートされており、Web APIやモバイルアプリ、JavaScriptなど様々な環境で扱えます。
DataSet
のデータをJSONに変換することで、異なるシステム間でのデータ連携が容易になります。
- 軽量で通信コストが低い
XMLなどの他のフォーマットに比べて、JSONは冗長なタグが少なく、データサイズが小さくなりやすいです。
これにより、ネットワーク通信時の負荷を軽減できます。
- 構造がシンプルで扱いやすい
JSONはオブジェクトと配列の組み合わせでデータを表現します。
DataSet
のテーブルや行の構造を自然にマッピングできるため、変換後のデータを直感的に理解しやすいです。
- 多くのライブラリでサポートされている
C#ではSystem.Text.Json
やNewtonsoft.Json
などの強力なJSONシリアライザがあり、簡単にDataSet
やDataTable
をJSONに変換できます。
これらのライブラリは高速かつ柔軟な設定が可能です。
- 人間が読み書きしやすい
JSONはテキスト形式なので、デバッグやログの確認時に内容を直接確認しやすいです。
これにより、開発や運用の効率が向上します。
このように、JSONフォーマットはDataSet
のデータを外部に渡したり保存したりする際に非常に適しています。
DataSetが選ばれる場面
DataSet
は.NETアプリケーションでデータを扱う際に広く使われていますが、特に以下のようなシナリオで選ばれることが多いです。
- 複数のテーブルをまとめて扱いたい場合
複数の関連テーブルを一括で管理し、テーブル間のリレーションを保持したいときにDataSet
が便利です。
例えば、親子関係のあるマスターデータと詳細データを同時に扱う場合などです。
- オフラインでのデータ操作が必要な場合
データベースから取得したデータを一時的にメモリ上で保持し、ユーザーが編集や追加を行った後にまとめて更新したい場合に適しています。
DataSet
は変更履歴を管理できるため、差分更新が可能です。
- データベースに依存しないデータ操作
DataSet
はデータベースに依存せずにデータを操作できるため、データベースがない環境や、複数のデータソースを統合したい場合に使われます。
- 複雑なデータ構造を扱う場合
テーブル間の制約やリレーションを設定し、整合性を保ちながらデータを操作したい場合にDataSet
が役立ちます。
- レガシーシステムとの互換性
古い.NETアプリケーションや既存のコードベースでDataSet
が使われている場合、新しい機能を追加する際にもDataSet
を継続して利用することがあります。
ただし、近年はEntity FrameworkやDapperなどのORM(Object-Relational Mapping)ツールが普及し、DataSet
の利用は減少傾向にあります。
しかし、DataSet
は依然として柔軟で汎用性が高いため、特定のシナリオでは選択肢として有効です。
JSONに変換することで、DataSet
のデータをWeb APIのレスポンスや外部システムとのデータ連携に活用しやすくなります。
特に複数テーブルのデータをまとめて送信したい場合に、DataSet
の構造をそのままJSONにマッピングできるのは大きなメリットです。
主要ライブラリ比較
System.Text.Json
System.Text.Json
は、.NET Core 3.0以降で標準搭載されたJSONシリアライザです。
Microsoftが公式に提供しており、パフォーマンスと軽量さに重点を置いています。
DataSet
やDataTable
のシリアライズもサポートしていますが、標準のままだとDataSet
の複雑な構造を完全に反映するのはやや制限があります。
特徴としては以下の点が挙げられます。
- 高速かつ低メモリ消費
ネイティブに最適化されており、特に.NET Core環境で高速に動作します。
大規模データのシリアライズに向いています。
- 軽量で依存関係が少ない
追加の外部ライブラリを必要とせず、標準ライブラリとして利用可能です。
- 柔軟なカスタマイズが可能
JsonSerializerOptions
でプロパティ名の変換や無視設定、エンコーディングなど細かく制御できます。
DataSet
のシリアライズ
DataSet
やDataTable
を直接シリアライズ可能ですが、リレーション情報やスキーマ情報は含まれません。
単純なテーブルデータの変換に適しています。
- 制限点
複雑な型やカスタムシリアライザの実装はやや手間がかかる場合があります。
また、DataSet
のリレーションや制約をJSONに反映させたい場合は追加実装が必要です。
サンプルコード(DataTable
のシリアライズ例):
using System;
using System.Data;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Encodings.Web;
class Program
{
static void Main()
{
// DataTableを作成し、"Users"という名前を付ける
DataTable table = new DataTable("Users");
// テーブルに"Id"列(整数型)と"Name"列(文字列型)を追加する
table.Columns.Add("Id", typeof(int));
table.Columns.Add("Name", typeof(string));
// 行を追加:1行目はID=1で名前は"佐藤"
table.Rows.Add(1, "佐藤");
// 2行目はID=2で名前は"高橋"
table.Rows.Add(2, "高橋");
// DataTableの行を辞書のリストに変換する準備
var rowsList = new List<Dictionary<string, object>>();
// DataTableの各行について処理
foreach (DataRow row in table.Rows)
{
// 各行のデータを保存する辞書を作成
var dict = new Dictionary<string, object>();
// その行の各列ごとに
foreach (DataColumn col in table.Columns)
{
// 辞書に「列名」をキーとして、実際の値をセット
dict[col.ColumnName] = row[col];
}
// 1行分の辞書をリストに追加する
rowsList.Add(dict);
}
// JSONシリアライズの設定を作成する
var options = new JsonSerializerOptions
{
// 日本語などの文字列をそのままエスケープせずに出力できるように設定
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
// 辞書のリストをJSON文字列に変換する
string json = JsonSerializer.Serialize(rowsList, options);
// JSON文字列をコンソールに表示する
Console.WriteLine(json);
}
}
[{"Id":1,"Name":"佐藤"},{"Id":2,"Name":"高橋"}]
Newtonsoft.Json
Newtonsoft.Json
(別名Json.NET)は、.NETで最も広く使われているJSONライブラリです。
柔軟性が高く、複雑なオブジェクトのシリアライズやカスタマイズに強みがあります。
DataSet
やDataTable
のシリアライズに関しても豊富な機能を備えています。
Newtonsoft.Json
は、Nugetからインストールする必要があります。
「Newtonsoft.Json」と検索してインストールするようにしてください。

dotnet add package Newtonsoft.Json
主な特徴は以下の通りです。
- 高い互換性と柔軟性
複雑な型やカスタムコンバータの実装が容易で、DataSet
のリレーションやスキーマ情報を含めたシリアライズも可能です。
DataSet
専用のコンバータが用意されている
JsonConvert.SerializeObject
でDataSet
を渡すと、テーブル名をキーにしたJSONオブジェクトとして変換されます。
複数テーブルの構造を自然に表現できます。
- 豊富な設定オプション
プロパティの命名規則変更、Null値の扱い、循環参照の制御など細かい調整が可能です。
- 広範なコミュニティサポート
多くのサンプルやドキュメントがあり、トラブルシュートもしやすいです。
サンプルコード(DataSet
のシリアライズ例):
using System;
using System.Data;
using Newtonsoft.Json;
class Program
{
static void Main()
{
DataSet dataSet = new DataSet();
DataTable table = new DataTable("Products");
table.Columns.Add("ProductId", typeof(int));
table.Columns.Add("ProductName", typeof(string));
table.Rows.Add(101, "ノートパソコン");
table.Rows.Add(102, "スマートフォン");
dataSet.Tables.Add(table);
string json = JsonConvert.SerializeObject(dataSet, Formatting.Indented);
Console.WriteLine(json);
}
}
{
"Products": [
{
"ProductId": 101,
"ProductName": "ノートパソコン"
},
{
"ProductId": 102,
"ProductName": "スマートフォン"
}
]
}
Utf8Json
Utf8Json
は、高速なJSONシリアライズ・デシリアライズを目指したライブラリで、バイナリベースのUTF-8処理に特化しています。
パフォーマンス重視の環境でよく使われますが、DataSet
やDataTable
の標準サポートは限定的です。
特徴は以下の通りです。
- 非常に高速な処理速度
UTF-8バイト列を直接操作し、GC発生を抑えた設計で高速なシリアライズを実現しています。
- 軽量で依存関係が少ない
ネイティブコードに近いパフォーマンスを目指し、余計な機能を省いています。
DataSet
のサポートは限定的
標準ではDataSet
やDataTable
のシリアライズに対応していません。
カスタムフォーマットや独自のシリアライザを実装する必要があります。
- 用途が限定される
高速処理が必要なAPIサーバーやゲーム開発などで使われることが多いですが、汎用的なデータ構造のシリアライズには向かない場合があります。
DataSet
を扱う場合は、DataTable
単位でJSONに変換し、必要に応じてカスタムシリアライザを作成することが多いです。

Jil
Jil
は、Stack Overflowの開発者が作成した高速JSONシリアライザで、パフォーマンスと使いやすさを両立しています。
DataSet
の直接サポートはありませんが、DataTable
やPOCOクラスのシリアライズに適しています。

特徴は以下の通りです。
- 高速なシリアライズ・デシリアライズ
内部でIL生成を活用し、実行時に最適化されたコードを生成するため高速です。
- シンプルなAPI
使い方が簡単で、設定も少なくすぐに利用できます。
DataSet
のサポートは限定的
直接DataSet
をシリアライズする機能はなく、DataTable
単位での変換や、POCOに変換してからのシリアライズが推奨されます。
- 制限事項
柔軟なカスタマイズや複雑な型のサポートは他のライブラリに比べて少なめです。
サンプルコード(DataTable
のシリアライズ例):
using System;
using System.Data;
using Jil;
class Program
{
static void Main()
{
DataTable table = new DataTable("Employees");
table.Columns.Add("EmployeeId", typeof(int));
table.Columns.Add("EmployeeName", typeof(string));
table.Rows.Add(1, "山田");
table.Rows.Add(2, "佐々木");
string json = JSON.Serialize(table);
Console.WriteLine(json);
}
}
[{"EmployeeId":1,"EmployeeName":"山田"},{"EmployeeId":2,"EmployeeName":"佐々木"}]
これらのライブラリはそれぞれ特徴が異なり、用途や環境に応じて使い分けることが重要です。
System.Text.Json
は標準搭載で軽量、Newtonsoft.Json
は柔軟性が高く多機能、Utf8Json
は高速処理に特化、Jil
はシンプルかつ高速なシリアライズを実現しています。
DataSet
の複雑な構造を扱う場合はNewtonsoft.Json
が最も扱いやすい傾向にあります。
System.Text.JsonによるSerialize
基本的なSerializeステップ
System.Text.Json
を使ってDataSet
やDataTable
をJSONに変換する際の基本的な流れは、まず対象のデータを準備し、JsonSerializer.Serialize
メソッドを呼び出すだけです。
ただし、DataSet
は複数のDataTable
を含むため、丸ごと変換する場合とテーブル単位で変換する場合で手順が異なります。
DataSetを丸ごと変換
System.Text.Json
はDataSet
を直接シリアライズできますが、内部的にはDataTable
の配列として扱われます。
DataSet
全体を1つのJSONオブジェクトに変換したい場合は、カスタム変換処理が必要になることが多いです。
標準のままだとテーブル名がキーとして反映されず、単純な配列として出力されるためです。
以下はDataSet
を丸ごとシリアライズする簡単な例です。
using System;
using System.Data;
using System.Linq;
using System.Text.Json;
using System.Text.Encodings.Web;
class Program
{
static void Main()
{
DataSet dataSet = new DataSet();
DataTable table1 = new DataTable("Users");
table1.Columns.Add("Id", typeof(int));
table1.Columns.Add("Name", typeof(string));
table1.Rows.Add(1, "佐藤");
table1.Rows.Add(2, "鈴木");
dataSet.Tables.Add(table1);
DataTable table2 = new DataTable("Orders");
table2.Columns.Add("OrderId", typeof(int));
table2.Columns.Add("UserId", typeof(int));
table2.Rows.Add(1001, 1);
table2.Rows.Add(1002, 2);
dataSet.Tables.Add(table2);
// Usersデータ抽出
var users = dataSet.Tables["Users"].AsEnumerable()
.Select(row => new
{
Id = row.Field<int>("Id"),
Name = row.Field<string>("Name")
});
// Ordersデータ抽出
var orders = dataSet.Tables["Orders"].AsEnumerable()
.Select(row => new
{
OrderId = row.Field<int>("OrderId"),
UserId = row.Field<int>("UserId")
});
var exportObj = new
{
Users = users,
Orders = orders
};
var options = new JsonSerializerOptions
{
WriteIndented = true,
// 日本語などの文字列をそのままエスケープせずに出力できるように設定
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
string json = JsonSerializer.Serialize(exportObj, options);
Console.WriteLine(json);
}
}
{
"Users": [
{
"Id": 1,
"Name": "佐藤"
},
{
"Id": 2,
"Name": "鈴木"
}
],
"Orders": [
{
"OrderId": 1001,
"UserId": 1
},
{
"OrderId": 1002,
"UserId": 2
}
]
}
このように、DataSet
はテーブルの配列としてシリアライズされ、テーブル名は含まれません。
テーブル名をキーにしたオブジェクト形式にしたい場合は、後述のカスタム処理が必要です。
DataTable単位で変換
System.Text.Json
は DataTable
のシリアライズを直接サポートしておらず、特に DataTable.Columns
に含まれる System.Type
をシリアライズしようとしたときに例外が発生します。
これを回避するために、DataTable
の行データだけを取り出し、シリアライズ可能な形でシリアライズする方法があります。
以下のコードでは、DataTable
の各行を辞書に変換し、そのリストをシリアライズしています。
using System;
using System.Collections.Generic;
using System.Data;
using System.Text.Json;
using System.Text.Encodings.Web;
class Program
{
static void Main()
{
DataTable table = new DataTable("Users");
table.Columns.Add("Id", typeof(int));
table.Columns.Add("Name", typeof(string));
table.Rows.Add(1, "田中");
table.Rows.Add(2, "山本");
var rowsList = new List<Dictionary<string, object>>();
foreach (DataRow row in table.Rows)
{
var dict = new Dictionary<string, object>();
foreach (DataColumn col in table.Columns)
{
dict[col.ColumnName] = row[col];
}
rowsList.Add(dict);
}
var options = new JsonSerializerOptions
{
WriteIndented = true,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
string json = JsonSerializer.Serialize(rowsList, options);
Console.WriteLine(json);
}
}
[
{
"Id": 1,
"Name": "田中"
},
{
"Id": 2,
"Name": "山本"
}
]
このコードでは、DataTable
のスキーマ情報を含めず、行データのみをシリアライズしているため、System.Type
の問題は発生しません。
オプション設定の詳細
System.Text.Json
のJsonSerializerOptions
を使うことで、シリアライズの挙動を細かく制御できます。
DataSet
やDataTable
のJSON変換時に特に役立つオプションを紹介します。
PropertyNamingPolicy
プロパティ名の変換ルールを指定できます。
デフォルトはC#のプロパティ名をそのまま使いますが、JSONの慣習に合わせてキャメルケースに変換したい場合に設定します。
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
この設定を使うと、Id
がid
、UserName
がuserName
のように変換されます。
APIの仕様に合わせて使い分けると良いでしょう。
IgnoreNullValues
null
値のプロパティをJSONに含めるかどうかを制御します。
true
に設定すると、null
のプロパティは出力されません。
var options = new JsonSerializerOptions
{
IgnoreNullValues = true
};
DataTable
の列にDBNull
が含まれる場合、null
として扱われるため、不要なnull
プロパティを省略したいときに便利です。
Encoder設定
日本語などの非ASCII文字をエスケープせずにそのまま出力したい場合は、JavaScriptEncoder
を設定します。
using System.Text.Encodings.Web;
using System.Text.Unicode;
var options = new JsonSerializerOptions
{
Encoder = JavaScriptEncoder.Create(UnicodeRanges.All)
};
これにより、"名前": "山田"
のように日本語がそのままJSONに含まれ、可読性が向上します。
デフォルトでは一部の文字がUnicodeエスケープされるため、見やすさを重視する場合に設定します。
ハマりやすいポイント
System.Text.Json
でDataSet
やDataTable
をシリアライズする際に注意すべき点をまとめます。
DataSet
のテーブル名がJSONに含まれない
標準のJsonSerializer.Serialize
では、DataSet
は単なるDataTable
の配列として扱われ、テーブル名はJSONに含まれません。
テーブル名をキーにしたオブジェクト形式にしたい場合は、Dictionary<string, DataTable>
に変換してからシリアライズするなどの工夫が必要です。
DBNull
はnull
に変換される
DataTable
の空のセルはDBNull.Value
ですが、System.Text.Json
ではnull
として扱われます。
これにより、null
値の扱いに注意が必要です。
- 循環参照の扱い
DataSet
のリレーションで親子テーブルが循環参照を持つ場合、標準のシリアライズでは例外が発生します。
循環参照を含む場合は、リレーションを外すか、カスタムシリアライザを実装する必要があります。
- 型情報は含まれない
JSONはスキーマレスなフォーマットなので、DataSet
の列の型情報や制約はシリアライズされません。
復元時に型を正しく扱うためには、別途スキーマ情報を管理する必要があります。
- 大文字・小文字の違いに注意
JSONのプロパティ名は大文字・小文字を区別します。
APIの仕様に合わせてPropertyNamingPolicy
を設定しないと、受け側で認識されないことがあります。
これらのポイントを理解し、必要に応じてカスタム変換やオプション設定を行うことで、System.Text.Json
を使ったDataSet
のJSON変換をスムーズに進められます。
Newtonsoft.JsonによるSerialize
基本的なSerializeコードパターン
Newtonsoft.Json
(Json.NET)を使ったDataSet
やDataTable
のシリアライズは非常にシンプルです。
JsonConvert.SerializeObject
メソッドに対象のDataSet
やDataTable
を渡すだけで、テーブル名をキーにしたJSONオブジェクトや、行の配列としてJSONが生成されます。
Newtonsoft.Json
は、Nugetからインストールする必要があります。
「Newtonsoft.Json」と検索してインストールするようにしてください。

dotnet add package Newtonsoft.Json
以下はDataSet
をシリアライズする基本的なコード例です。
using System;
using System.Data;
using Newtonsoft.Json;
class Program
{
static void Main()
{
DataSet dataSet = new DataSet();
DataTable table = new DataTable("Employees");
table.Columns.Add("EmployeeId", typeof(int));
table.Columns.Add("EmployeeName", typeof(string));
table.Rows.Add(1, "田中");
table.Rows.Add(2, "佐藤");
dataSet.Tables.Add(table);
string json = JsonConvert.SerializeObject(dataSet, Formatting.Indented);
Console.WriteLine(json);
}
}
{
"Employees": [
{
"EmployeeId": 1,
"EmployeeName": "田中"
},
{
"EmployeeId": 2,
"EmployeeName": "佐藤"
}
]
}
このように、DataSet
の各DataTable
がJSONオブジェクトのキーとなり、テーブルの行が配列として表現されます。
Formatting.Indented
を指定すると見やすい整形済みJSONが出力されます。
JsonConvert.SerializeObjectの拡張
JsonConvert.SerializeObject
はオプションを指定して挙動を拡張できます。
特にJsonSerializerSettings
を使うことで、シリアライズの細かい制御が可能です。
主な設定例を示します。
- Null値の扱い
NullValueHandling.Ignore
を指定すると、null
のプロパティはJSONに含まれません。
- 日付フォーマットの指定
DateFormatString
で日付の出力形式をカスタマイズできます。
- 循環参照の制御
ReferenceLoopHandling.Ignore
で循環参照を無視し、例外を防止します。
- カスタムコンバータの追加
Converters
に独自のJsonConverter
を登録して、特定の型のシリアライズ方法を変更できます。
以下は設定を使った例です。
var settings = new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Ignore,
DateFormatString = "yyyy-MM-dd",
ReferenceLoopHandling = ReferenceLoopHandling.Ignore
};
string json = JsonConvert.SerializeObject(dataSet, Formatting.Indented, settings);
Console.WriteLine(json);
このように設定を活用することで、API仕様や要件に合わせたJSON出力が可能です。
DataSetConverterの活用
Newtonsoft.Json
にはDataSet
やDataTable
専用のコンバータが組み込まれており、これを活用することでより柔軟なシリアライズができます。
通常は自動的に適用されますが、明示的に指定することも可能です。
例えば、JsonSerializerSettings
のConverters
にDataSetConverter
を追加すると、DataSet
のテーブル名をキーにしたJSONオブジェクトとしてシリアライズされます。
using Newtonsoft.Json.Converters;
var settings = new JsonSerializerSettings();
settings.Converters.Add(new DataSetConverter());
string json = JsonConvert.SerializeObject(dataSet, Formatting.Indented, settings);
Console.WriteLine(json);
DataSetConverter
はDataSet
の構造を正確に反映し、テーブル名や行データを適切にJSONにマッピングします。
複数テーブルを含むDataSet
のシリアライズに最適です。
カスタムContractResolver
ContractResolver
は、シリアライズ時にどのプロパティを含めるか、名前をどう変換するかなどを制御する仕組みです。
Newtonsoft.Json
ではDefaultContractResolver
を継承して独自のルールを実装できます。
例えば、DataTable
の列名をすべて小文字に変換したい場合、以下のようなカスタムContractResolver
を作成できます。
using Newtonsoft.Json.Serialization;
using System.Reflection;
class LowercaseContractResolver : DefaultContractResolver
{
protected override string ResolvePropertyName(string propertyName)
{
return propertyName.ToLowerInvariant();
}
}
これをJsonSerializerSettings
に設定してシリアライズします。
var settings = new JsonSerializerSettings
{
ContractResolver = new LowercaseContractResolver(),
Formatting = Formatting.Indented
};
string json = JsonConvert.SerializeObject(dataSet, settings);
Console.WriteLine(json);
{
"employees": [
{
"employeeid": 1,
"employeename": "田中"
},
{
"employeeid": 2,
"employeename": "佐藤"
}
]
}
このように、ContractResolver
を使うとプロパティ名の変換や除外、条件付きシリアライズなど細かい制御が可能です。
DataSet
やDataTable
のJSON変換で命名規則を統一したい場合に役立ちます。
カスタムシリアライザ実装
DataReaderでストリーミング出力
大量データを扱う場合、DataSet
やDataTable
を一度にメモリ上に展開してからJSONに変換すると、メモリ消費が大きくなりパフォーマンスが低下します。
そこで、IDataReader
(例えばSqlDataReader
)を使ってデータを逐次読み込みながらJSONをストリーミング出力する方法が有効です。
この方法では、データを1行ずつ読み込み、JSONの配列要素として順次書き出します。
メモリに全データを保持しないため、大規模データのシリアライズに適しています。
以下はIDataReader
を使ってJSON配列をストリーミング出力するサンプルです。
using System;
using System.Data;
using System.Text.Json;
using System.IO;
using System.Text.Encodings.Web;
class Program
{
static void Main()
{
// ここでは例としてDataTableをIDataReaderに変換して使用
DataTable table = new DataTable();
table.Columns.Add("Id", typeof(int));
table.Columns.Add("Name", typeof(string));
table.Rows.Add(1, "田中");
table.Rows.Add(2, "鈴木");
using var reader = table.CreateDataReader();
using var stream = new MemoryStream();
// 日本語がユニコードエスケープされないようにエンコーダを設定
var options = new JsonWriterOptions
{
Indented = true,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
using var writer = new Utf8JsonWriter(stream, options);
writer.WriteStartArray();
while (reader.Read())
{
writer.WriteStartObject();
for (int i = 0; i < reader.FieldCount; i++)
{
string name = reader.GetName(i);
object value = reader.GetValue(i);
if (value == DBNull.Value)
{
writer.WriteNull(name);
}
else if (value is int intValue)
{
writer.WriteNumber(name, intValue);
}
else if (value is string strValue)
{
writer.WriteString(name, strValue);
}
else
{
writer.WriteString(name, value.ToString());
}
}
writer.WriteEndObject();
}
writer.WriteEndArray();
writer.Flush();
string json = System.Text.Encoding.UTF8.GetString(stream.ToArray());
Console.WriteLine(json);
}
}
[
{
"Id": 1,
"Name": "田中"
},
{
"Id": 2,
"Name": "鈴木"
}
]
このように、IDataReader
の行を1つずつ読み込みながらUtf8JsonWriter
でJSONを生成することで、メモリ効率よく高速にJSONを出力できます。
実際のデータベース接続時も同様の手法でストリーミング処理が可能です。
Utf8JsonWriterで高速生成
System.Text.Json
のUtf8JsonWriter
は、低レベルのJSON書き込みAPIであり、文字列連結や中間オブジェクト生成を避けて直接バイト列を生成します。
これにより、非常に高速かつメモリ効率の良いJSON生成が可能です。
Utf8JsonWriter
を使う場合、JSONの構造を手動で制御しながら書き込むため、柔軟かつ高速なシリアライズが実現できます。
特に大量データやリアルタイム処理で効果を発揮します。
以下はDataTable
の内容をUtf8JsonWriter
で高速にJSON化する例です。
using System;
using System.Data;
using System.Text.Json;
using System.IO;
using System.Text.Encodings.Web;
class Program
{
static void Main()
{
DataTable table = new DataTable("Products");
table.Columns.Add("ProductId", typeof(int));
table.Columns.Add("ProductName", typeof(string));
table.Rows.Add(101, "ノートPC");
table.Rows.Add(102, "スマホ");
using var stream = new MemoryStream();
// 日本語がユニコードエスケープされないようにエンコーダを設定
var options = new JsonWriterOptions
{
Indented = true,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
using var writer = new Utf8JsonWriter(stream, options);
writer.WriteStartArray();
foreach (DataRow row in table.Rows)
{
writer.WriteStartObject();
foreach (DataColumn col in table.Columns)
{
string colName = col.ColumnName;
object val = row[col];
if (val == DBNull.Value)
{
writer.WriteNull(colName);
}
else if (val is int intVal)
{
writer.WriteNumber(colName, intVal);
}
else if (val is string strVal)
{
writer.WriteString(colName, strVal);
}
else
{
writer.WriteString(colName, val.ToString());
}
}
writer.WriteEndObject();
}
writer.WriteEndArray();
writer.Flush();
string json = System.Text.Encoding.UTF8.GetString(stream.ToArray());
Console.WriteLine(json);
}
}
[
{
"ProductId": 101,
"ProductName": "ノートPC"
},
{
"ProductId": 102,
"ProductName": "スマホ"
}
]
この方法はJsonSerializer.Serialize
よりも高速で、メモリ割り当てを抑えられるため、大量データの高速シリアライズに適しています。
CharBuffer再利用テクニック
JSONシリアライズ時のパフォーマンスをさらに向上させるために、文字列バッファchar[]
の再利用は有効なテクニックです。
特に大量の小さな文字列を連結したり、繰り返し同じ構造のJSONを生成する場合に効果があります。
Utf8JsonWriter
はバイト配列に直接書き込みますが、文字列操作が必要な場面ではSpan<char>
やArrayPool<char>
を活用してバッファを再利用することでGC発生を抑制できます。
以下はArrayPool<char>
を使って文字列バッファを再利用し、JSONのキーや値の組み立てに活用する例です。
using System;
using System.Buffers;
using System.Text;
class Program
{
static void Main()
{
var pool = ArrayPool<char>.Shared;
char[] buffer = pool.Rent(256);
try
{
string key = "Name";
string value = "山田太郎";
int length = Encoding.UTF8.GetChars(Encoding.UTF8.GetBytes(value), 0, Encoding.UTF8.GetByteCount(value), buffer, 0);
// ここでは単純にバッファの内容を文字列化して表示
string reusedString = new string(buffer, 0, value.Length);
Console.WriteLine($"{key}: {reusedString}");
}
finally
{
pool.Return(buffer);
}
}
}
この例は単純化していますが、実際にはJSONのキーや値を組み立てる際にArrayPool<char>
で確保したバッファを使い回すことで、文字列生成のコストを削減できます。
Utf8JsonWriter
と組み合わせると、バイト配列のバッファもArrayPool<byte>
で再利用可能です。
これにより、GC負荷を大幅に減らし、シリアライズ処理のスループットを向上させられます。
これらのカスタムシリアライザ実装テクニックを活用することで、DataSet
やDataTable
のJSON変換を高速かつメモリ効率良く行えます。
特に大規模データやリアルタイム処理が求められるシナリオで効果的です。
性能最適化
メモリ使用量の削減
DataSet
やDataTable
をJSONに変換する際、特に大規模データの場合はメモリ使用量がパフォーマンスに大きく影響します。
メモリ消費を抑えるためのポイントをいくつか紹介します。
- ストリーミングシリアライズの活用
一度に全データをメモリ上に展開してからシリアライズするのではなく、IDataReader
やUtf8JsonWriter
を使って逐次的にJSONを書き出す方法が効果的です。
これにより、メモリに保持するデータ量を最小限に抑えられます。
- バッファの再利用
文字列やバイト配列のバッファをArrayPool<T>
で再利用することで、GC(ガベージコレクション)の発生を減らし、メモリ断片化を防げます。
特に大量の文字列連結やバイト書き込みが発生する場合に有効です。
- 不要なデータの除外
JSONに含める必要のない列や行を事前にフィルタリングし、シリアライズ対象から外すことでメモリ使用量を削減できます。
JsonSerializerOptions
やJsonConverter
で特定のプロパティを無視する設定も活用しましょう。
- Null値の省略
IgnoreNullValues
System.Text.Json
やNullValueHandling.Ignore
Newtonsoft.Json
を設定し、null
値のプロパティをJSONに含めないようにすると、出力サイズが小さくなりメモリ負荷も軽減されます。
- 型変換の最適化
不要なボクシングや文字列変換を避けるため、シリアライズ時に型ごとに適切な書き込みメソッドを使うことが重要です。
例えば、int
やdouble
は数値として直接書き込み、文字列化は最小限に抑えます。
これらの対策を組み合わせることで、メモリ使用量を抑えつつ高速なJSON変換が可能になります。
並列変換でスループット向上
複数のDataTable
を含むDataSet
や、大量の独立したデータをJSONに変換する場合、並列処理を活用してスループットを向上させることができます。
- テーブル単位の並列処理
DataSet
内の各DataTable
は独立しているため、Parallel.ForEach
などを使って複数テーブルのシリアライズを同時に実行できます。
これによりCPUコアを有効活用し、全体の処理時間を短縮できます。
- バッチ単位の分割処理
大量の行を持つDataTable
は、行を複数のバッチに分割し、それぞれを別スレッドでシリアライズする方法もあります。
バッチごとにJSON配列を生成し、後で結合する形で処理します。
- スレッドセーフなライブラリの利用
並列処理時は、使用するJSONライブラリやシリアライザがスレッドセーフであることを確認してください。
System.Text.Json
はスレッドセーフですが、Newtonsoft.Json
の一部機能はスレッドセーフでない場合があります。
- I/O待ちの非同期化
ファイルやネットワークへの書き込みが伴う場合は、非同期APIを活用してI/O待ち時間を短縮し、CPUリソースを効率的に使うことも重要です。
以下はParallel.ForEach
を使ったテーブル単位の並列シリアライズ例です。
using System;
using System.Data;
using System.Text.Json;
using System.Threading.Tasks;
using System.Collections.Concurrent;
class Program
{
static void Main()
{
DataSet dataSet = new DataSet();
// 複数のDataTableを追加(省略)
var results = new ConcurrentDictionary<string, string>();
Parallel.ForEach(dataSet.Tables.Cast<DataTable>(), table =>
{
string json = JsonSerializer.Serialize(table);
results[table.TableName] = json;
});
foreach (var kvp in results)
{
Console.WriteLine($"Table: {kvp.Key}");
Console.WriteLine(kvp.Value);
}
}
}
このように並列化することで、複数テーブルの変換を高速化できます。
大規模データの分割とバッチ処理
非常に大きなDataTable
やDataSet
を一括でJSONに変換すると、メモリ不足やタイムアウトの原因になります。
そこで、大規模データは分割してバッチ処理するのが効果的です。
- 行単位の分割
DataTable
の行を一定数ずつ分割し、複数の小さなJSON配列に分けてシリアライズします。
これにより、一度に扱うデータ量を制限し、メモリ消費を抑えられます。
- ページング処理との連携
データベースからの取得時にページングを行い、ページ単位でJSON変換・送信する方法もあります。
これにより、サーバー負荷やネットワーク負荷を分散できます。
- バッチごとの非同期処理
分割したバッチを非同期で処理し、処理完了後に結果を結合または順次送信することで、全体のスループットを向上させられます。
- 部分的なJSON結合
分割したJSON配列を後で結合する場合は、配列の開始[
と終了]
を適切に処理し、カンマ区切りで連結する必要があります。
以下はDataTable
の行をバッチに分割してJSONに変換する例です。
using System;
using System.Data;
using System.Text.Json;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
DataTable table = new DataTable("Logs");
table.Columns.Add("Id", typeof(int));
table.Columns.Add("Message", typeof(string));
for (int i = 1; i <= 10000; i++)
{
table.Rows.Add(i, $"ログメッセージ{i}");
}
int batchSize = 1000;
int totalRows = table.Rows.Count;
List<string> jsonBatches = new List<string>();
for (int offset = 0; offset < totalRows; offset += batchSize)
{
int count = Math.Min(batchSize, totalRows - offset);
var batchRows = table.AsEnumerable().Skip(offset).Take(count);
// DataRowの各列をディクショナリに変換
var batchData = batchRows.Select(row =>
table.Columns.Cast<DataColumn>().ToDictionary(
col => col.ColumnName,
col => row[col]
)
).ToList();
string json = JsonSerializer.Serialize(batchData);
jsonBatches.Add(json);
}
// jsonBatchesに分割されたJSONが格納されている
Console.WriteLine($"バッチ数: {jsonBatches.Count}");
// 最初のバッチの先頭3件を表示して中身を確認
Console.WriteLine("最初のバッチの先頭3件:");
var firstBatch = JsonSerializer.Deserialize<List<Dictionary<string, object>>>(jsonBatches[0]);
for (int i = 0; i < 3; i++)
{
var item = firstBatch[i];
Console.WriteLine($"Id: {item["Id"]}, Message: {item["Message"]}");
}
}
}
バッチ数: 10
最初のバッチの先頭3件:
Id: 1, Message: ログメッセージ1
Id: 2, Message: ログメッセージ2
Id: 3, Message: ログメッセージ3
この方法で大規模データを分割し、メモリ負荷を抑えつつ安定したJSON変換を実現できます。
バッチ処理はAPIレスポンスの分割送信やファイル分割保存にも応用可能です。
型とフォーマットの扱い
数値型と精度の落とし穴
DataSet
やDataTable
の数値型をJSONに変換する際、特に浮動小数点数float
やdouble
やdecimal
型の精度に注意が必要です。
JSONは数値を文字列ではなく数値リテラルとして表現しますが、JavaScriptの数値表現はIEEE 754の倍精度浮動小数点数に基づいているため、精度の損失が起こることがあります。
例えば、非常に大きな整数や高精度の小数をJSONにシリアライズすると、JavaScript側で丸められたり、誤差が生じる可能性があります。
これにより、受け取った側で値が変わってしまうリスクがあります。
対策としては以下の方法があります。
- 文字列としてシリアライズする
精度が重要な数値は、JSONに文字列として出力し、受け取り側で適切にパースする方法です。
Newtonsoft.Json
のJsonConverter
やSystem.Text.Json
のカスタムコンバータで実装可能です。
decimal
型の利用
.NET側でdecimal
型を使うと高精度の数値を保持できますが、JSONに変換するときは上記の精度問題に注意が必要です。
- 必要な桁数に丸める
小数点以下の桁数を制限して丸めてからシリアライズすることで、誤差を抑えられます。
以下はdecimal
を文字列としてシリアライズする例(Newtonsoft.Json
のカスタムコンバータ)です。
using Newtonsoft.Json;
using System;
class DecimalToStringConverter : JsonConverter<decimal>
{
public override void WriteJson(JsonWriter writer, decimal value, JsonSerializer serializer)
{
writer.WriteValue(value.ToString());
}
public override decimal ReadJson(JsonReader reader, Type objectType, decimal existingValue, bool hasExistingValue, JsonSerializer serializer)
{
return decimal.Parse((string)reader.Value);
}
}
このように精度を保つための工夫が必要です。
DateTimeとTimeZone
DateTime
型のシリアライズは、タイムゾーンやフォーマットの扱いでトラブルが起きやすいポイントです。
JSONは日時を文字列として表現しますが、フォーマットやタイムゾーン情報が明示されていないと、受け取り側で誤解されることがあります。
主な注意点は以下の通りです。
- ISO 8601形式の利用
標準的な日時フォーマットとしてyyyy-MM-ddTHH:mm:ssZ
(UTC)やyyyy-MM-ddTHH:mm:ss±hh:mm
(オフセット付き)を使うのが推奨されます。
System.Text.Json
やNewtonsoft.Json
はデフォルトでISO 8601形式をサポートしています。
- タイムゾーンの明示
DateTime
のKind
プロパティUtc
、Local
、Unspecified
に応じて、UTCに変換してからシリアライズするか、ローカルタイムをそのまま出力するかを決める必要があります。
Unspecified
は曖昧なので注意が必要です。
DateTimeOffset
の利用
タイムゾーン情報を含めて日時を扱いたい場合は、DateTimeOffset
型を使うと安全です。
JSONにオフセット付きの日時文字列としてシリアライズされます。
- カスタムフォーマットの指定
Newtonsoft.Json
のDateFormatString
やSystem.Text.Json
のカスタムコンバータでフォーマットを指定できます。
以下はNewtonsoft.Json
でUTC日時をISO 8601形式でシリアライズする例です。
var settings = new JsonSerializerSettings
{
DateTimeZoneHandling = DateTimeZoneHandling.Utc,
DateFormatString = "yyyy-MM-ddTHH:mm:ssZ"
};
string json = JsonConvert.SerializeObject(DateTime.Now, settings);
Console.WriteLine(json);
DBNullとnullの互換性
DataTable
の空セルはDBNull.Value
で表現されますが、JSONにはnull
としてシリアライズされます。
DBNull
は.NETのデータベース特有の概念であり、JSONには存在しないため、変換時にnull
にマッピングされるのが一般的です。
注意点は以下の通りです。
DBNull
はnull
に変換される
シリアライズ時にDBNull.Value
はnull
として出力されます。
受け取り側でnull
を適切に扱う必要があります。
- 逆変換時の扱い
JSONのnull
をDataTable
に戻す際は、DBNull.Value
に変換する処理が必要です。
標準のデシリアライズでは自動的に変換されないことが多いため、カスタム処理を実装することが多いです。
null
を含む列の型に注意
DataColumn
の型が値型(int
やDateTime
など)でAllowDBNull
がfalse
の場合、null
を受け入れられず例外が発生することがあります。
AllowDBNull
をtrue
に設定するか、Nullable型を使うことが推奨されます。
以下はDBNull
をnull
に変換してシリアライズする例です。
foreach (DataRow row in table.Rows)
{
foreach (DataColumn col in table.Columns)
{
object val = row[col];
if (val == DBNull.Value)
{
// JSONではnullとして扱う
val = null;
}
// valをJSONに書き込む処理
}
}
Enumとカスタム型
DataTable
の列にenum
型や独自のカスタム型を使う場合、JSONへのシリアライズとデシリアライズで注意が必要です。
- Enumのシリアライズ
デフォルトではenum
は整数値としてシリアライズされますが、文字列名で出力したい場合はNewtonsoft.Json
のStringEnumConverter
やSystem.Text.Json
のJsonStringEnumConverter
を使います。
using Newtonsoft.Json.Converters;
var settings = new JsonSerializerSettings();
settings.Converters.Add(new StringEnumConverter());
string json = JsonConvert.SerializeObject(enumValue, settings);
- カスタム型のシリアライズ
独自クラスや構造体をDataTable
の列に使う場合、標準のJSONシリアライザは対応できないことがあります。
カスタムJsonConverter
を実装して、シリアライズ・デシリアライズの方法を定義する必要があります。
- 複雑な型の展開
カスタム型のプロパティを展開してJSONに含めるか、単一の文字列やIDで表現するかは設計次第です。
パフォーマンスや可読性を考慮して選択します。
- 型情報の付与
JSONはスキーマレスなので、型情報を保持したい場合は$type
などのプロパティを付加する方法もありますが、セキュリティリスクに注意が必要です。
これらのポイントを踏まえ、enum
やカスタム型を含むDataSet
のJSON変換は適切なコンバータや設定を用いて行うことが重要です。
リレーション付きDataSetの変換
親子テーブルのネスト戦略
DataSet
内で親子関係を持つ複数のDataTable
をJSONに変換する際、親子テーブルのデータをどのようにネスト(入れ子)構造として表現するかが重要です。
単純にテーブルごとに独立した配列として出力すると、親子関係がわかりにくくなります。
一般的なネスト戦略は以下の通りです。
- 親テーブルの各行に子テーブルの関連行を配列として埋め込む
親テーブルのJSONオブジェクトの中に、子テーブルの関連行を配列として含める方法です。
これにより、親子関係がJSONの階層構造として自然に表現されます。
- リレーションのキーを使って手動でネストを構築する
DataRelation
の親キーと子キーを利用し、親行ごとに子行を抽出してJSONオブジェクトに追加します。
DataSet
の標準シリアライズでは自動的にネストされないため、カスタム処理が必要です。
- 複数階層のネストも可能
子テーブルがさらに孫テーブルを持つ場合も同様に、再帰的にネスト構造を作成できます。
以下は親子テーブルをネストしてJSONに変換する簡単な例です。
using System;
using System.Data;
using System.Text.Json;
using System.Text.Encodings.Web;
using System.Collections.Generic;
class Program
{
static void Main()
{
DataSet ds = new DataSet();
DataTable parent = new DataTable("Parent");
parent.Columns.Add("Id", typeof(int));
parent.Columns.Add("Name", typeof(string));
parent.Rows.Add(1, "親1");
parent.Rows.Add(2, "親2");
ds.Tables.Add(parent);
DataTable child = new DataTable("Child");
child.Columns.Add("Id", typeof(int));
child.Columns.Add("ParentId", typeof(int));
child.Columns.Add("Value", typeof(string));
child.Rows.Add(1, 1, "子1-1");
child.Rows.Add(2, 1, "子1-2");
child.Rows.Add(3, 2, "子2-1");
ds.Tables.Add(child);
ds.Relations.Add("ParentChild", parent.Columns["Id"], child.Columns["ParentId"]);
var result = new List<object>();
foreach (DataRow parentRow in parent.Rows)
{
var children = new List<object>();
foreach (DataRow childRow in parentRow.GetChildRows("ParentChild"))
{
children.Add(new
{
Id = childRow["Id"],
Value = childRow["Value"]
});
}
result.Add(new
{
Id = parentRow["Id"],
Name = parentRow["Name"],
Children = children
});
}
var options = new JsonSerializerOptions
{
WriteIndented = true,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
string json = JsonSerializer.Serialize(result, options);
Console.WriteLine(json);
}
}
[
{
"Id": 1,
"Name": "親1",
"Children": [
{
"Id": 1,
"Value": "子1-1"
},
{
"Id": 2,
"Value": "子1-2"
}
]
},
{
"Id": 2,
"Name": "親2",
"Children": [
{
"Id": 3,
"Value": "子2-1"
}
]
}
]
このように親行に子行の配列をネストすることで、JSONで親子関係をわかりやすく表現できます。
参照整合性を保つ方法
DataSet
のリレーションは親子テーブル間の参照整合性を保証しますが、JSONに変換する際はこの整合性を維持するために注意が必要です。
- 親子関係の一貫性を保つ
親テーブルの行が存在しないのに子テーブルの行が存在する、または逆の状態が起きないように、JSON変換前にDataSet
の整合性を検証します。
DataSet
のEnforceConstraints
プロパティをtrue
にしておくと、整合性違反があれば例外が発生します。
- リレーションキーの明示的な出力
JSONに親子関係を示すキー(外部キー)を含めることで、受け取り側で関係を再構築しやすくなります。
特にネストしない場合は必須です。
- 循環参照の回避
複雑なリレーションで循環参照がある場合、JSONシリアライズ時に無限ループや例外が発生することがあります。
循環を検出して除外するか、ネストの深さを制限する必要があります。
- 更新時の整合性維持
JSONからDataSet
に逆変換する際も、親子関係のキーが正しく設定されているか検証し、整合性を保つ処理を実装します。
スキーマ情報の付与
DataSet
のスキーマ情報(テーブル構造、列の型、制約、リレーションなど)をJSONに含めることで、受け取り側でデータ構造を正確に再現しやすくなります。
- スキーマとデータの分離
スキーマ情報を別のJSONオブジェクトやファイルとして出力し、データと分けて管理する方法があります。
これにより、スキーマの変更管理やバージョン管理が容易になります。
- スキーマをJSONに埋め込む
データと一緒にスキーマ情報を含める場合、DataTable
の列名、データ型、主キー、外部キー制約などをJSONのメタデータとして付加します。
例えば、以下のような構造が考えられます。
{
"Schema": {
"Tables": [
{
"TableName": "Parent",
"Columns": [
{ "ColumnName": "Id", "DataType": "Int32", "IsPrimaryKey": true },
{ "ColumnName": "Name", "DataType": "String" }
],
"Relations": [
{ "RelationName": "ParentChild", "ChildTable": "Child", "ParentColumns": ["Id"], "ChildColumns": ["ParentId"] }
]
}
]
},
"Data": {
"Parent": [ ... ],
"Child": [ ... ]
}
}
- スキーマ情報の活用
受け取り側でスキーマを解析し、型変換やバリデーション、リレーションの再構築に利用できます。
これにより、JSONからDataSet
を正確に復元可能です。
- 自動生成ツールの利用
DataSet
のGetXmlSchema
メソッドでXMLスキーマを取得し、これをJSONに変換する方法もあります。
既存のスキーマ定義を活用することで手間を減らせます。
スキーマ情報を付与することで、単なるデータのやり取りだけでなく、データ構造の理解や検証も可能になり、堅牢なシステム連携が実現します。
逆変換(Deserialize)手順
JSONからDataSetを生成
JSON形式のデータをDataSet
に逆変換(デシリアライズ)する際は、JSONの構造をDataSet
の複数のDataTable
にマッピングする必要があります。
特に、複数のテーブルが含まれる場合は、テーブル名をキーにしたJSONオブジェクトとして表現されていることが多いため、それを適切に読み込むことが重要です。
Newtonsoft.Json
を使った基本的な方法は、JSONをDictionary<string, DataTable>
としてデシリアライズし、その後DataSet
にテーブルを追加する手順です。
using System;
using System.Data;
using System.Collections.Generic;
using Newtonsoft.Json;
class Program
{
static void Main()
{
string json = @"
{
""Users"": [
{ ""Id"": 1, ""Name"": ""田中"" },
{ ""Id"": 2, ""Name"": ""鈴木"" }
],
""Orders"": [
{ ""OrderId"": 1001, ""UserId"": 1 },
{ ""OrderId"": 1002, ""UserId"": 2 }
]
}";
var tables = JsonConvert.DeserializeObject<Dictionary<string, DataTable>>(json);
DataSet dataSet = new DataSet();
foreach (var kvp in tables)
{
kvp.Value.TableName = kvp.Key;
dataSet.Tables.Add(kvp.Value);
}
// 確認用出力
foreach (DataTable table in dataSet.Tables)
{
Console.WriteLine($"Table: {table.TableName}");
foreach (DataRow row in table.Rows)
{
foreach (DataColumn col in table.Columns)
{
Console.Write($"{col.ColumnName}={row[col]} ");
}
Console.WriteLine();
}
}
}
}
Table: Users
Id=1 Name=田中
Id=2 Name=鈴木
Table: Orders
OrderId=1001 UserId=1
OrderId=1002 UserId=2
この方法では、JSONのキーがDataTable
の名前となり、各配列がテーブルの行として読み込まれます。
System.Text.Json
でも同様にDictionary<string, DataTable>
にデシリアライズ可能ですが、DataTable
のデシリアライズはNewtonsoft.Json
の方が柔軟です。
行単位の更新検出
DataSet
に対してJSONからデータを読み込む際、既存のデータと比較してどの行が追加・更新・削除されたかを検出することは重要です。
これにより、差分だけをデータベースに反映したり、UIの更新を効率化できます。
行単位の更新検出の基本的な流れは以下の通りです。
- 主キーの設定
DataTable
に主キーPrimaryKey
を設定しておくことで、行の一意性を判定できます。
- JSONから新しい
DataTable
を生成
JSONをデシリアライズして新しいDataTable
を作成します。
- 既存の
DataTable
と比較
主キーを使って既存の行と新しい行を照合し、以下の判定を行います。
- 新しい行が既存にない → 追加
- 主キーは同じだが内容が異なる → 更新
- 既存にあって新しい行にない → 削除候補
- 変更を反映
追加・更新・削除の操作をDataTable
に適用します。
以下は簡単な更新検出の例です。
// 既存のDataTableに主キーを設定
existingTable.PrimaryKey = new DataColumn[] { existingTable.Columns["Id"] };
// 新しいDataTableをJSONから生成(省略)
foreach (DataRow newRow in newTable.Rows)
{
var key = newRow["Id"];
var existingRow = existingTable.Rows.Find(key);
if (existingRow == null)
{
// 追加
existingTable.ImportRow(newRow);
}
else
{
// 更新判定(例としてName列のみ比較)
if (!existingRow["Name"].Equals(newRow["Name"]))
{
existingRow["Name"] = newRow["Name"];
}
}
}
// 削除判定は逆方向のループで行う
このように主キーを活用して効率的に差分を検出し、必要な更新だけを行うことができます。
スキーマ差異のハンドリング
JSONからDataSet
やDataTable
にデシリアライズする際、JSONの構造や型が既存のスキーマと異なる場合があります。
これを適切に処理しないと、例外が発生したりデータの不整合が起きます。
スキーマ差異の主な課題と対策は以下の通りです。
- 列の追加・削除
JSONに存在する列がDataTable
にない場合は、動的に列を追加するか、無視するかを選択します。
逆にDataTable
にあるがJSONにない列は、デフォルト値を設定するか、null
を許容する必要があります。
- 型の不一致
JSONの値の型がDataTable
の列の型と異なる場合、変換エラーが発生します。
例えば、文字列が数値列に入っている場合などです。
カスタム変換や例外処理で対応します。
- スキーマの自動生成
JSONの構造からDataTable
のスキーマを自動生成し、既存のスキーマとマージする方法もあります。
DataTable
のLoad
メソッドやReadXmlSchema
を活用するケースもあります。
- バージョン管理
APIやデータ仕様の変更に伴いスキーマが変わる場合は、バージョン情報を付与し、バージョンごとに適切な変換処理を実装することが望ましいです。
以下は列の追加を動的に行う例です。
foreach (var colName in newTable.Columns.Cast<DataColumn>().Select(c => c.ColumnName))
{
if (!existingTable.Columns.Contains(colName))
{
existingTable.Columns.Add(colName, newTable.Columns[colName].DataType);
}
}
スキーマ差異を適切にハンドリングすることで、柔軟かつ堅牢なJSONからDataSet
への逆変換が実現します。
エラーとトラブルシュート
文字コードに起因する例
JSONのシリアライズやデシリアライズ時に文字コードの問題が発生することがあります。
特に日本語などのマルチバイト文字を扱う場合、文字化けや不正な文字列が出力されるケースが多いです。
主な原因と対策は以下の通りです。
- エンコーディングの不一致
JSONをファイルやネットワークに書き出す際、UTF-8以外のエンコーディングで保存すると、読み込み時に文字化けが起こります。
System.Text.Json
やNewtonsoft.Json
はデフォルトでUTF-8を想定しているため、ファイルの読み書き時はUTF-8を明示的に指定しましょう。
- Unicodeエスケープの扱い
デフォルト設定では、一部の非ASCII文字が\uXXXX
形式でエスケープされることがあります。
これが原因で見た目が悪くなる場合は、System.Text.Json
のJavaScriptEncoder
をカスタマイズしてエスケープを抑制できます。
- BOM付きUTF-8の影響
UTF-8のBOM(Byte Order Mark)が付いているファイルを読み込むと、先頭に不正な文字が混入することがあります。
BOMなしのUTF-8で保存するか、読み込み時にBOMを正しく処理する必要があります。
- ストリームのエンコーディング指定漏れ
StreamReader
やStreamWriter
を使う際にエンコーディングを指定しないと、環境依存のデフォルトエンコーディングが使われ、文字化けの原因になります。
必ずEncoding.UTF8
を指定してください。
例:ファイルからJSONを読み込む際の正しいエンコーディング指定
using var reader = new StreamReader("data.json", Encoding.UTF8);
string json = reader.ReadToEnd();
型不一致例
DataSet
やDataTable
のJSON変換時に、型不一致によるエラーやデータ破損が発生することがあります。
特にデシリアライズ時にJSONの値の型が期待する型と異なる場合に問題が起きやすいです。
よくあるケースと対処法は以下の通りです。
- 数値と文字列の混在
JSONで数値型の列に文字列が入っていると、デシリアライズ時に例外が発生します。
API仕様やデータ生成元を確認し、型の整合性を保つことが重要です。
- nullとDBNullの違い
JSONのnull
はDBNull
に変換されますが、DataColumn
の型やAllowDBNull
設定によっては例外が発生します。
AllowDBNull
を適切に設定し、必要に応じてデフォルト値を用意してください。
- 日付型のフォーマット不一致
JSONの日付文字列が期待するフォーマットと異なると、DateTime
型への変換に失敗します。
ISO 8601形式を使うか、カスタムコンバータで対応しましょう。
- 列の型変更による不整合
既存のDataTable
に対して異なる型のデータを読み込もうとするとエラーになります。
スキーマ差異のハンドリングを行い、必要に応じて列の型を変更または新規追加してください。
例:int
型の列に文字列が入っている場合の例外
Newtonsoft.Json.JsonSerializationException: Could not convert string to integer: "abc".
Streamが閉じているエラー
JSONのシリアライズやデシリアライズ時にStream
が既に閉じているために発生するエラーはよくあります。
典型的な例は以下の通りです。
Stream
のライフサイクル管理ミス
Stream
やStreamReader
、StreamWriter
をusing
ブロックで囲んでいる場合、スコープ外でアクセスするとObjectDisposedException
が発生します。
シリアライズやデシリアライズの処理が完了するまでStream
を閉じないように管理してください。
- 非同期処理での競合
非同期メソッドでStream
を共有している場合、先に閉じられてしまうことがあります。
await
の使い方やStream
の共有範囲を見直しましょう。
JsonSerializer
のメソッド呼び出し順序
Utf8JsonWriter
を使う場合、Flush
やDispose
を呼んだ後にStream
にアクセスするとエラーになります。
Flush
は必要ですが、Dispose
はusing
で管理し、Stream
の状態を確認してから操作してください。
例:Stream
が閉じている場合の例外
System.ObjectDisposedException: Cannot access a closed Stream.
対策としては、Stream
の開閉を明確に管理し、シリアライズ処理中は閉じないようにすることが重要です。
例えば、以下のようにusing
の範囲を適切に設定します。
using var stream = new MemoryStream();
using var writer = new Utf8JsonWriter(stream);
// シリアライズ処理
writer.Flush();
// streamはここでまだ有効
string json = Encoding.UTF8.GetString(stream.ToArray());
これらのエラーは発生しやすいポイントですが、原因を理解し適切に対処することで安定したJSON変換処理を実現できます。
セキュリティ対策
型情報漏えいの防止
JSONシリアライズ時に型情報が漏えいすると、攻撃者に内部構造や実装の詳細を知られてしまい、セキュリティリスクが高まります。
特にNewtonsoft.Json
のTypeNameHandling
を使う場合は注意が必要です。
TypeNameHandling
の危険性
TypeNameHandling.All
やTypeNameHandling.Auto
を設定すると、JSONに.NETの型情報($type
プロパティ)が埋め込まれます。
これにより、リモートコード実行(RCE)などの脆弱性が発生する可能性があります。
- 型情報の埋め込みを避ける
可能な限りTypeNameHandling
は無効にし、型情報をJSONに含めない設定にします。
代わりに、明示的な型変換やカスタムコンバータで安全に処理します。
- 信頼できるデータのみで使用する
どうしても型情報を含める必要がある場合は、信頼できるソースからのデータに限定し、外部からの入力は絶対に受け付けないようにします。
- カスタム
SerializationBinder
の利用
Newtonsoft.Json
ではSerializationBinder
を実装して、許可された型のみをデシリアライズ可能に制限できます。
これにより、悪意ある型の読み込みを防止します。
例:安全なSerializationBinder
の設定例
class SafeBinder : Newtonsoft.Json.Serialization.ISerializationBinder
{
private static readonly HashSet<string> AllowedTypes = new HashSet<string>
{
"Namespace.YourAllowedType, YourAssembly"
};
public Type BindToType(string assemblyName, string typeName)
{
string fullName = $"{typeName}, {assemblyName}";
if (AllowedTypes.Contains(fullName))
{
return Type.GetType(fullName);
}
throw new JsonSerializationException("不正な型情報です。");
}
public void BindToName(Type serializedType, out string assemblyName, out string typeName)
{
assemblyName = null;
typeName = null;
}
}
このように型情報の漏えいを防ぐことが重要です。
予期しない巨大JSONへの対策
大量のデータをJSONに変換すると、巨大なJSON文字列が生成され、メモリ不足やサービス停止の原因になります。
悪意あるユーザーが意図的に巨大なJSONを送信する攻撃も考慮しなければなりません。
- サイズ制限の設定
JSONの受信時にサイズ上限を設け、一定以上の大きさのデータは拒否します。
ASP.NET CoreなどのWebフレームワークではリクエストボディの最大サイズを設定可能です。
- ストリーミング処理の活用
一度に全データを読み込まず、ストリーミングで部分的に処理することでメモリ消費を抑制します。
- バッチ処理やページングの導入
大量データは分割して複数回に分けて送受信し、一度に巨大なJSONを扱わない設計にします。
- タイムアウト設定
処理時間が長くなりすぎないようにタイムアウトを設定し、リソースの枯渇を防ぎます。
- ログ監視とアラート
異常に大きなJSONや処理時間の長いリクエストを検知し、管理者に通知する仕組みを用意します。
エスケープ処理でXSS防止
JSONをWebページで表示したり、JavaScriptに埋め込む場合、適切なエスケープ処理を行わないとクロスサイトスクリプティング(XSS)攻撃のリスクがあります。
- JSON文字列のエスケープ
JSON内の文字列は、"
や\
、制御文字などを正しくエスケープする必要があります。
System.Text.Json
やNewtonsoft.Json
は標準でこれを行いますが、手動で文字列を組み立てる場合は注意が必要です。
- HTMLコンテキストでのエスケープ
JSONをHTMLに埋め込む場合は、HTMLエスケープも必要です。
例えば、<script>
タグ内にJSONを直接書くと、</script>
などの文字列が悪用される可能性があります。
- JavaScriptエスケープの適用
JSONをJavaScriptの文字列リテラルとして埋め込む場合は、JavaScriptのエスケープルールに従ってエスケープします。
特にシングルクォートやダブルクォート、改行コードに注意が必要です。
- Content Security Policy (CSP)の活用
XSS対策としてCSPを設定し、スクリプトの実行を制限することも有効です。
- ライブラリの利用
信頼できるJSONシリアライザを使い、手動でのエスケープは避けることが推奨されます。
例:System.Text.Json
で日本語を含む文字列をエスケープせずに出力する設定
var options = new JsonSerializerOptions
{
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
string json = JsonSerializer.Serialize(yourObject, options);
この設定は可読性を高めますが、XSSリスクがある場合は慎重に使う必要があります。
これらのセキュリティ対策を適切に実施することで、DataSet
のJSON変換に伴うリスクを低減し、安全なシステム運用が可能になります。
テストと検証
単体テストパターン
DataSet
やDataTable
のJSON変換処理を確実に動作させるためには、単体テストを充実させることが重要です。
単体テストでは、シリアライズ・デシリアライズの正確性や例外処理、境界値などを検証します。
主なテストパターンは以下の通りです。
- 正常系テスト
- 空の
DataSet
やDataTable
をJSONに変換し、期待通りの空配列や空オブジェクトが生成されるか - 複数テーブルを含む
DataSet
のシリアライズ・デシリアライズが正しく行われるか - 親子リレーションを持つ
DataSet
のネスト構造が正しく表現されるか
- 空の
- 異常系テスト
null
値やDBNull
を含むデータの扱いが正しいか- 型不一致や不正なJSON入力に対して例外が発生するか、適切にハンドリングされるか
- 空文字列や特殊文字を含むデータのエスケープ処理が正しいか
- 境界値テスト
- 大量の行や列を持つ
DataTable
の処理が正常に行われるか - 最大長の文字列や特殊文字を含むデータの処理
- 大量の行や列を持つ
- パフォーマンステスト
- シリアライズ・デシリアライズの処理時間が許容範囲内か
単体テストはxUnit
やNUnit
などのテストフレームワークを使い、自動化して実行することが推奨されます。
Goldenファイルによる比較
Goldenファイルテストは、期待される出力(Goldenファイル)と実際の出力を比較することで、変換処理の正確性を検証する手法です。
JSON変換のテストにおいては、生成されるJSON文字列をGoldenファイルとして保存し、変更があった場合に差分を検出します。
Goldenファイルテストのメリットは以下の通りです。
- 出力の完全一致を保証
JSONの構造や値が意図せず変更されていないかを厳密にチェックできます。
- リファクタリング時の安全性向上
コード変更による副作用を早期に発見できます。
- 複雑なデータ構造の検証に有効
手動で期待値を記述するのが難しい場合でも、Goldenファイルを基準にできます。
実装例としては、テスト実行時にJSONをファイルに書き出し、既存のGoldenファイルと比較します。
差分があればテスト失敗とし、差分内容をログに出力します。
string actualJson = JsonSerializer.Serialize(dataSet, new JsonSerializerOptions { WriteIndented = true });
string goldenPath = "GoldenFiles/expected.json";
if (!File.Exists(goldenPath))
{
File.WriteAllText(goldenPath, actualJson);
Assert.Fail("Goldenファイルが存在しなかったため作成しました。内容を確認してください。");
}
string expectedJson = File.ReadAllText(goldenPath);
Assert.Equal(expectedJson, actualJson);
JSONの整形やキーの順序に注意し、比較が安定するように工夫することが重要です。
性能ベンチマークの自動化
JSON変換処理の性能を継続的に監視するために、ベンチマークテストを自動化することが効果的です。
これにより、パフォーマンスの劣化を早期に検知し、最適化の効果を定量的に評価できます。
ベンチマーク自動化のポイントは以下の通りです。
- ベンチマークフレームワークの利用
BenchmarkDotNet
などの専用ライブラリを使い、正確かつ再現性の高い測定を行います。
- 代表的なシナリオの選定
実際の利用ケースに近いデータサイズや構造を用意し、シリアライズ・デシリアライズの処理時間やメモリ使用量を測定。
- CI/CDパイプラインへの組み込み
GitHub ActionsやAzure DevOpsなどのCI環境で定期的にベンチマークを実行し、結果をレポート化します。
- 閾値設定とアラート
パフォーマンスが一定の閾値を超えた場合に通知を行い、問題を早期に発見。
以下はBenchmarkDotNet
を使った簡単なベンチマーク例です。
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System.Data;
using System.Text.Json;
public class JsonSerializationBenchmark
{
private DataTable table;
[GlobalSetup]
public void Setup()
{
table = new DataTable("Sample");
table.Columns.Add("Id", typeof(int));
table.Columns.Add("Name", typeof(string));
for (int i = 0; i < 1000; i++)
{
table.Rows.Add(i, $"名前{i}");
}
}
[Benchmark]
public string SerializeDataTable()
{
return JsonSerializer.Serialize(table);
}
}
class Program
{
static void Main()
{
var summary = BenchmarkRunner.Run<JsonSerializationBenchmark>();
}
}
このように自動化されたベンチマークにより、継続的に性能を監視し、品質を維持できます。
現場で役立つTips
動的列追加への対応
DataTable
やDataSet
を扱う際、実行時に列が動的に追加されるケースがあります。
例えば、外部データの仕様変更やユーザー入力により、予め定義されていない列が含まれることがあります。
このような場合、JSON変換時に列の存在を前提とした処理が失敗することがあるため、柔軟に対応する必要があります。
対応方法のポイントは以下の通りです。
- 列の存在チェックを行う
シリアライズやデシリアライズの前に、対象の列がDataTable.Columns
に存在するかを確認し、存在しない場合は追加するかスキップします。
- 列の型を動的に推定する
新規列の型が不明な場合は、最初の数行のデータから型を推測するか、string
型として扱うことで汎用的に対応可能です。
- JSONからのデシリアライズ時に列を追加
JSONに含まれるキーがDataTable
の列にない場合、動的に列を追加してからデータを読み込む処理を実装します。
- 例外処理で安全に対応
予期しない列があっても例外を投げずにログを残すなど、堅牢な処理を心がけます。
以下はJSONデシリアライズ時に動的に列を追加する例です。
foreach (var key in jsonObject.Keys)
{
if (!dataTable.Columns.Contains(key))
{
dataTable.Columns.Add(key, typeof(string)); // 型は必要に応じて調整
}
}
このように動的列追加に対応することで、仕様変更や不確定なデータ構造にも柔軟に対応できます。
古いJSON構造との互換性
システムのバージョンアップやAPI変更に伴い、JSONの構造が変わることがあります。
古いJSON構造との互換性を保つためには、以下の工夫が役立ちます。
- バージョン管理の導入
JSONにバージョン情報を含め、受け取り側でバージョンに応じた処理を分岐させます。
- カスタムデシリアライズ処理
古い形式のJSONを新しいDataSet
構造にマッピングする変換ロジックを実装し、互換性を維持します。
- デフォルト値の設定
新しいスキーマで追加された列が古いJSONに存在しない場合、デフォルト値を設定して処理を継続します。
- 柔軟なJSONパース
必要なキーだけを抽出し、不要なキーは無視することで、構造の違いによるエラーを回避します。
- テストケースの充実
古いJSONサンプルを用意し、互換性テストを自動化して品質を保証します。
例:古いJSONで欠落している列にデフォルト値を設定するコード例
if (!dataTable.Columns.Contains("NewColumn"))
{
dataTable.Columns.Add("NewColumn", typeof(string));
}
foreach (DataRow row in dataTable.Rows)
{
if (row.IsNull("NewColumn"))
{
row["NewColumn"] = "デフォルト値";
}
}
これにより、古いJSONでも新しいシステムで問題なく処理できます。
LINQでの前処理
DataSet
やDataTable
のJSON変換前に、LINQを活用してデータの前処理を行うと効率的かつ簡潔に操作できます。
LINQはデータのフィルタリング、並べ替え、集計、変換などに強力な表現力を持っています。
活用例は以下の通りです。
- 不要な行の除外
条件に合わない行を除外してからシリアライズします。
- 列の選択や変換
必要な列だけを抽出した匿名型のコレクションに変換し、JSONに出力します。
- 集計やグルーピング
データをグループ化して集計結果をJSONに含める。
- ネスト構造の生成
親子関係のあるテーブルをLINQで結合し、ネストしたオブジェクトを作成します。
以下はDataTable
の行をLINQでフィルタリングし、必要な列だけを匿名型に変換してJSONにシリアライズする例です。
var filtered = from row in dataTable.AsEnumerable()
where row.Field<int>("Age") >= 20
select new
{
Id = row.Field<int>("Id"),
Name = row.Field<string>("Name")
};
string json = JsonSerializer.Serialize(filtered);
Console.WriteLine(json);
このようにLINQを使うことで、複雑な前処理を簡潔に記述でき、JSON変換の柔軟性と可読性が向上します。
まとめ
この記事では、C#のDataSet
をJSONに高速かつ正確に変換するためのテクニックと逆変換手順を詳しく解説しました。
主要なJSONライブラリの特徴やカスタムシリアライザの実装方法、性能最適化や型・フォーマットの扱い、リレーション付きデータのネスト方法など、実務で役立つポイントを網羅しています。
さらに、エラー対策やセキュリティ、テスト手法、現場での応用例も紹介し、安全かつ効率的なJSON変換を実現するための知識が身につきます。