ファイル

【C#】DataSetをJSONへ高速変換するSerializeテクニックと逆変換手順

C#ではDataSetをJSONへ変換する際、System.Text.JsonNewtonsoft.JsonDataTableあるいはDataSet全体をSerializeできます。

テーブル名がキーとなり各行が配列要素です。

nullや型、日付のフォーマットを確認し列名を安定させることで双方向変換がスムーズに進みます。

目次から探す
  1. DataSetとJSON変換の基本
  2. 主要ライブラリ比較
  3. System.Text.JsonによるSerialize
  4. Newtonsoft.JsonによるSerialize
  5. カスタムシリアライザ実装
  6. 性能最適化
  7. 型とフォーマットの扱い
  8. リレーション付きDataSetの変換
  9. 逆変換(Deserialize)手順
  10. エラーとトラブルシュート
  11. セキュリティ対策
  12. テストと検証
  13. 現場で役立つTips
  14. まとめ

DataSetとJSON変換の基本

DataSetとDataTableの相互関係

C#のDataSetは、複数のDataTableをまとめて管理できるデータ構造です。

DataTableは行と列で構成される表形式のデータを表し、DataSetはこれらのテーブルを複数持つことができます。

DataSetは単なるテーブルの集合体ではなく、テーブル間のリレーション(親子関係)や制約も管理できるため、複雑なデータ構造を扱う際に便利です。

具体的には、DataSetは以下のような役割を持っています。

  • 複数のDataTableを格納し、一つのまとまりとして扱う
  • テーブル間のリレーションを定義し、親子関係を管理する
  • データの整合性を保つための制約(主キー、外部キーなど)を設定できる
  • データの変更履歴を追跡し、更新や削除の管理が可能

一方、DataTableは単一の表を表現し、列DataColumnと行DataRowで構成されます。

DataTableはデータベースのテーブルに似た構造で、列ごとに型が定義されているため、型安全にデータを扱えます。

DataSetDataTableの関係は、以下のようにイメージできます。

構造体役割
DataSet複数のDataTableをまとめ、テーブル間の関係や制約を管理するコンテナ
DataTable行と列で構成される単一の表形式データ

JSONに変換する際は、DataSetの中の各DataTableを個別にJSON配列として表現し、それらをまとめて1つのJSONオブジェクトにすることが多いです。

これにより、元のテーブル構造を保ったままデータを表現できます。

JSONフォーマットの利点

JSON(JavaScript Object Notation)は、軽量で人間にも読みやすいテキスト形式のデータ交換フォーマットです。

C#のDataSetをJSONに変換することには多くの利点があります。

  1. 言語やプラットフォームの互換性が高い

JSONは多くのプログラミング言語でサポートされており、Web APIやモバイルアプリ、JavaScriptなど様々な環境で扱えます。

DataSetのデータをJSONに変換することで、異なるシステム間でのデータ連携が容易になります。

  1. 軽量で通信コストが低い

XMLなどの他のフォーマットに比べて、JSONは冗長なタグが少なく、データサイズが小さくなりやすいです。

これにより、ネットワーク通信時の負荷を軽減できます。

  1. 構造がシンプルで扱いやすい

JSONはオブジェクトと配列の組み合わせでデータを表現します。

DataSetのテーブルや行の構造を自然にマッピングできるため、変換後のデータを直感的に理解しやすいです。

  1. 多くのライブラリでサポートされている

C#ではSystem.Text.JsonNewtonsoft.Jsonなどの強力なJSONシリアライザがあり、簡単にDataSetDataTableをJSONに変換できます。

これらのライブラリは高速かつ柔軟な設定が可能です。

  1. 人間が読み書きしやすい

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が公式に提供しており、パフォーマンスと軽量さに重点を置いています。

DataSetDataTableのシリアライズもサポートしていますが、標準のままだとDataSetの複雑な構造を完全に反映するのはやや制限があります。

特徴としては以下の点が挙げられます。

  • 高速かつ低メモリ消費

ネイティブに最適化されており、特に.NET Core環境で高速に動作します。

大規模データのシリアライズに向いています。

  • 軽量で依存関係が少ない

追加の外部ライブラリを必要とせず、標準ライブラリとして利用可能です。

  • 柔軟なカスタマイズが可能

JsonSerializerOptionsでプロパティ名の変換や無視設定、エンコーディングなど細かく制御できます。

  • DataSetのシリアライズ

DataSetDataTableを直接シリアライズ可能ですが、リレーション情報やスキーマ情報は含まれません。

単純なテーブルデータの変換に適しています。

  • 制限点

複雑な型やカスタムシリアライザの実装はやや手間がかかる場合があります。

また、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ライブラリです。

柔軟性が高く、複雑なオブジェクトのシリアライズやカスタマイズに強みがあります。

DataSetDataTableのシリアライズに関しても豊富な機能を備えています。

Newtonsoft.Jsonのインストール

Newtonsoft.Jsonは、Nugetからインストールする必要があります。

「Newtonsoft.Json」と検索してインストールするようにしてください。

dotnet add package Newtonsoft.Json

主な特徴は以下の通りです。

  • 高い互換性と柔軟性

複雑な型やカスタムコンバータの実装が容易で、DataSetのリレーションやスキーマ情報を含めたシリアライズも可能です。

  • DataSet専用のコンバータが用意されている

JsonConvert.SerializeObjectDataSetを渡すと、テーブル名をキーにした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処理に特化しています。

パフォーマンス重視の環境でよく使われますが、DataSetDataTableの標準サポートは限定的です。

特徴は以下の通りです。

  • 非常に高速な処理速度

UTF-8バイト列を直接操作し、GC発生を抑えた設計で高速なシリアライズを実現しています。

  • 軽量で依存関係が少ない

ネイティブコードに近いパフォーマンスを目指し、余計な機能を省いています。

  • DataSetのサポートは限定的

標準ではDataSetDataTableのシリアライズに対応していません。

カスタムフォーマットや独自のシリアライザを実装する必要があります。

  • 用途が限定される

高速処理が必要な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を使ってDataSetDataTableをJSONに変換する際の基本的な流れは、まず対象のデータを準備し、JsonSerializer.Serializeメソッドを呼び出すだけです。

ただし、DataSetは複数のDataTableを含むため、丸ごと変換する場合とテーブル単位で変換する場合で手順が異なります。

DataSetを丸ごと変換

System.Text.JsonDataSetを直接シリアライズできますが、内部的には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.JsonDataTable のシリアライズを直接サポートしておらず、特に 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.JsonJsonSerializerOptionsを使うことで、シリアライズの挙動を細かく制御できます。

DataSetDataTableのJSON変換時に特に役立つオプションを紹介します。

PropertyNamingPolicy

プロパティ名の変換ルールを指定できます。

デフォルトはC#のプロパティ名をそのまま使いますが、JSONの慣習に合わせてキャメルケースに変換したい場合に設定します。

var options = new JsonSerializerOptions
{
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};

この設定を使うと、IdidUserNameuserNameのように変換されます。

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.JsonDataSetDataTableをシリアライズする際に注意すべき点をまとめます。

  • DataSetのテーブル名がJSONに含まれない

標準のJsonSerializer.Serializeでは、DataSetは単なるDataTableの配列として扱われ、テーブル名はJSONに含まれません。

テーブル名をキーにしたオブジェクト形式にしたい場合は、Dictionary<string, DataTable>に変換してからシリアライズするなどの工夫が必要です。

  • DBNullnullに変換される

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)を使ったDataSetDataTableのシリアライズは非常にシンプルです。

JsonConvert.SerializeObjectメソッドに対象のDataSetDataTableを渡すだけで、テーブル名をキーにしたJSONオブジェクトや、行の配列としてJSONが生成されます。

Newtonsoft.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にはDataSetDataTable専用のコンバータが組み込まれており、これを活用することでより柔軟なシリアライズができます。

通常は自動的に適用されますが、明示的に指定することも可能です。

例えば、JsonSerializerSettingsConvertersDataSetConverterを追加すると、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);

DataSetConverterDataSetの構造を正確に反映し、テーブル名や行データを適切に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を使うとプロパティ名の変換や除外、条件付きシリアライズなど細かい制御が可能です。

DataSetDataTableのJSON変換で命名規則を統一したい場合に役立ちます。

カスタムシリアライザ実装

DataReaderでストリーミング出力

大量データを扱う場合、DataSetDataTableを一度にメモリ上に展開してから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.JsonUtf8JsonWriterは、低レベルの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負荷を大幅に減らし、シリアライズ処理のスループットを向上させられます。

これらのカスタムシリアライザ実装テクニックを活用することで、DataSetDataTableのJSON変換を高速かつメモリ効率良く行えます。

特に大規模データやリアルタイム処理が求められるシナリオで効果的です。

性能最適化

メモリ使用量の削減

DataSetDataTableをJSONに変換する際、特に大規模データの場合はメモリ使用量がパフォーマンスに大きく影響します。

メモリ消費を抑えるためのポイントをいくつか紹介します。

  • ストリーミングシリアライズの活用

一度に全データをメモリ上に展開してからシリアライズするのではなく、IDataReaderUtf8JsonWriterを使って逐次的にJSONを書き出す方法が効果的です。

これにより、メモリに保持するデータ量を最小限に抑えられます。

  • バッファの再利用

文字列やバイト配列のバッファをArrayPool<T>で再利用することで、GC(ガベージコレクション)の発生を減らし、メモリ断片化を防げます。

特に大量の文字列連結やバイト書き込みが発生する場合に有効です。

  • 不要なデータの除外

JSONに含める必要のない列や行を事前にフィルタリングし、シリアライズ対象から外すことでメモリ使用量を削減できます。

JsonSerializerOptionsJsonConverterで特定のプロパティを無視する設定も活用しましょう。

  • Null値の省略

IgnoreNullValuesSystem.Text.JsonNullValueHandling.IgnoreNewtonsoft.Jsonを設定し、null値のプロパティをJSONに含めないようにすると、出力サイズが小さくなりメモリ負荷も軽減されます。

  • 型変換の最適化

不要なボクシングや文字列変換を避けるため、シリアライズ時に型ごとに適切な書き込みメソッドを使うことが重要です。

例えば、intdoubleは数値として直接書き込み、文字列化は最小限に抑えます。

これらの対策を組み合わせることで、メモリ使用量を抑えつつ高速な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);
        }
    }
}

このように並列化することで、複数テーブルの変換を高速化できます。

大規模データの分割とバッチ処理

非常に大きなDataTableDataSetを一括で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レスポンスの分割送信やファイル分割保存にも応用可能です。

型とフォーマットの扱い

数値型と精度の落とし穴

DataSetDataTableの数値型をJSONに変換する際、特に浮動小数点数floatdoubledecimal型の精度に注意が必要です。

JSONは数値を文字列ではなく数値リテラルとして表現しますが、JavaScriptの数値表現はIEEE 754の倍精度浮動小数点数に基づいているため、精度の損失が起こることがあります。

例えば、非常に大きな整数や高精度の小数をJSONにシリアライズすると、JavaScript側で丸められたり、誤差が生じる可能性があります。

これにより、受け取った側で値が変わってしまうリスクがあります。

対策としては以下の方法があります。

  • 文字列としてシリアライズする

精度が重要な数値は、JSONに文字列として出力し、受け取り側で適切にパースする方法です。

Newtonsoft.JsonJsonConverterSystem.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.JsonNewtonsoft.JsonはデフォルトでISO 8601形式をサポートしています。

  • タイムゾーンの明示

DateTimeKindプロパティUtcLocalUnspecifiedに応じて、UTCに変換してからシリアライズするか、ローカルタイムをそのまま出力するかを決める必要があります。

Unspecifiedは曖昧なので注意が必要です。

  • DateTimeOffsetの利用

タイムゾーン情報を含めて日時を扱いたい場合は、DateTimeOffset型を使うと安全です。

JSONにオフセット付きの日時文字列としてシリアライズされます。

  • カスタムフォーマットの指定

Newtonsoft.JsonDateFormatStringSystem.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にマッピングされるのが一般的です。

注意点は以下の通りです。

  • DBNullnullに変換される

シリアライズ時にDBNull.Valuenullとして出力されます。

受け取り側でnullを適切に扱う必要があります。

  • 逆変換時の扱い

JSONのnullDataTableに戻す際は、DBNull.Valueに変換する処理が必要です。

標準のデシリアライズでは自動的に変換されないことが多いため、カスタム処理を実装することが多いです。

  • nullを含む列の型に注意

DataColumnの型が値型(intDateTimeなど)でAllowDBNullfalseの場合、nullを受け入れられず例外が発生することがあります。

AllowDBNulltrueに設定するか、Nullable型を使うことが推奨されます。

以下はDBNullnullに変換してシリアライズする例です。

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.JsonStringEnumConverterSystem.Text.JsonJsonStringEnumConverterを使います。

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の整合性を検証します。

DataSetEnforceConstraintsプロパティを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を正確に復元可能です。

  • 自動生成ツールの利用

DataSetGetXmlSchemaメソッドで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の更新を効率化できます。

行単位の更新検出の基本的な流れは以下の通りです。

  1. 主キーの設定

DataTableに主キーPrimaryKeyを設定しておくことで、行の一意性を判定できます。

  1. JSONから新しいDataTableを生成

JSONをデシリアライズして新しいDataTableを作成します。

  1. 既存のDataTableと比較

主キーを使って既存の行と新しい行を照合し、以下の判定を行います。

  • 新しい行が既存にない → 追加
  • 主キーは同じだが内容が異なる → 更新
  • 既存にあって新しい行にない → 削除候補
  1. 変更を反映

追加・更新・削除の操作を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からDataSetDataTableにデシリアライズする際、JSONの構造や型が既存のスキーマと異なる場合があります。

これを適切に処理しないと、例外が発生したりデータの不整合が起きます。

スキーマ差異の主な課題と対策は以下の通りです。

  • 列の追加・削除

JSONに存在する列がDataTableにない場合は、動的に列を追加するか、無視するかを選択します。

逆にDataTableにあるがJSONにない列は、デフォルト値を設定するか、nullを許容する必要があります。

  • 型の不一致

JSONの値の型がDataTableの列の型と異なる場合、変換エラーが発生します。

例えば、文字列が数値列に入っている場合などです。

カスタム変換や例外処理で対応します。

  • スキーマの自動生成

JSONの構造からDataTableのスキーマを自動生成し、既存のスキーマとマージする方法もあります。

DataTableLoadメソッドや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.JsonNewtonsoft.JsonはデフォルトでUTF-8を想定しているため、ファイルの読み書き時はUTF-8を明示的に指定しましょう。

  • Unicodeエスケープの扱い

デフォルト設定では、一部の非ASCII文字が\uXXXX形式でエスケープされることがあります。

これが原因で見た目が悪くなる場合は、System.Text.JsonJavaScriptEncoderをカスタマイズしてエスケープを抑制できます。

  • BOM付きUTF-8の影響

UTF-8のBOM(Byte Order Mark)が付いているファイルを読み込むと、先頭に不正な文字が混入することがあります。

BOMなしのUTF-8で保存するか、読み込み時にBOMを正しく処理する必要があります。

  • ストリームのエンコーディング指定漏れ

StreamReaderStreamWriterを使う際にエンコーディングを指定しないと、環境依存のデフォルトエンコーディングが使われ、文字化けの原因になります。

必ずEncoding.UTF8を指定してください。

例:ファイルからJSONを読み込む際の正しいエンコーディング指定

using var reader = new StreamReader("data.json", Encoding.UTF8);
string json = reader.ReadToEnd();

型不一致例

DataSetDataTableのJSON変換時に、型不一致によるエラーやデータ破損が発生することがあります。

特にデシリアライズ時にJSONの値の型が期待する型と異なる場合に問題が起きやすいです。

よくあるケースと対処法は以下の通りです。

  • 数値と文字列の混在

JSONで数値型の列に文字列が入っていると、デシリアライズ時に例外が発生します。

API仕様やデータ生成元を確認し、型の整合性を保つことが重要です。

  • nullとDBNullの違い

JSONのnullDBNullに変換されますが、DataColumnの型やAllowDBNull設定によっては例外が発生します。

AllowDBNullを適切に設定し、必要に応じてデフォルト値を用意してください。

  • 日付型のフォーマット不一致

JSONの日付文字列が期待するフォーマットと異なると、DateTime型への変換に失敗します。

ISO 8601形式を使うか、カスタムコンバータで対応しましょう。

  • 列の型変更による不整合

既存のDataTableに対して異なる型のデータを読み込もうとするとエラーになります。

スキーマ差異のハンドリングを行い、必要に応じて列の型を変更または新規追加してください。

例:int型の列に文字列が入っている場合の例外

Newtonsoft.Json.JsonSerializationException: Could not convert string to integer: "abc".

Streamが閉じているエラー

JSONのシリアライズやデシリアライズ時にStreamが既に閉じているために発生するエラーはよくあります。

典型的な例は以下の通りです。

  • Streamのライフサイクル管理ミス

StreamStreamReaderStreamWriterusingブロックで囲んでいる場合、スコープ外でアクセスするとObjectDisposedExceptionが発生します。

シリアライズやデシリアライズの処理が完了するまでStreamを閉じないように管理してください。

  • 非同期処理での競合

非同期メソッドでStreamを共有している場合、先に閉じられてしまうことがあります。

awaitの使い方やStreamの共有範囲を見直しましょう。

  • JsonSerializerのメソッド呼び出し順序

Utf8JsonWriterを使う場合、FlushDisposeを呼んだ後にStreamにアクセスするとエラーになります。

Flushは必要ですが、Disposeusingで管理し、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.JsonTypeNameHandlingを使う場合は注意が必要です。

  • TypeNameHandlingの危険性

TypeNameHandling.AllTypeNameHandling.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.JsonNewtonsoft.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変換に伴うリスクを低減し、安全なシステム運用が可能になります。

テストと検証

単体テストパターン

DataSetDataTableのJSON変換処理を確実に動作させるためには、単体テストを充実させることが重要です。

単体テストでは、シリアライズ・デシリアライズの正確性や例外処理、境界値などを検証します。

主なテストパターンは以下の通りです。

  • 正常系テスト
    • 空のDataSetDataTableをJSONに変換し、期待通りの空配列や空オブジェクトが生成されるか
    • 複数テーブルを含むDataSetのシリアライズ・デシリアライズが正しく行われるか
    • 親子リレーションを持つDataSetのネスト構造が正しく表現されるか
  • 異常系テスト
    • null値やDBNullを含むデータの扱いが正しいか
    • 型不一致や不正なJSON入力に対して例外が発生するか、適切にハンドリングされるか
    • 空文字列や特殊文字を含むデータのエスケープ処理が正しいか
  • 境界値テスト
    • 大量の行や列を持つDataTableの処理が正常に行われるか
    • 最大長の文字列や特殊文字を含むデータの処理
  • パフォーマンステスト
    • シリアライズ・デシリアライズの処理時間が許容範囲内か

単体テストはxUnitNUnitなどのテストフレームワークを使い、自動化して実行することが推奨されます。

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

動的列追加への対応

DataTableDataSetを扱う際、実行時に列が動的に追加されるケースがあります。

例えば、外部データの仕様変更やユーザー入力により、予め定義されていない列が含まれることがあります。

このような場合、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での前処理

DataSetDataTableの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変換を実現するための知識が身につきます。

関連記事

Back to top button
目次へ