System.Text.JsonでJSONをDataTableに変換する方法|.NET Framework 4.7.2の現実的解決策

.NET Framework 4.7.2 で JSON を DataTable に入れたいとき、Newtonsoft.Json なら一発でも、System.Text.Json に切り替えると途端にうまくいかないことがあります。本記事では「Deserialize<DataTable> はできるのか?」の結論、つまずく理由、現実的な回避策(手組み・JsonConverter 自作・Newtonsoft 継続)を具体例付きで整理します。

目次

結論:System.Text.Json は DataTable を標準サポートしない

先に結論から言うと、System.Text.Json には DataSet / DataTable といった ADO.NET 系の型を“標準機能として”扱う組み込みサポートがありません。そのため、Newtonsoft.Json のように JsonSerializer.Deserialize<DataTable>() と書くだけで期待通りに変換できるケースは多くありません。

「JSON → DataTable」を System.Text.Json で実現するには、主に次のいずれかの方針になります。

  • 実務最短で済ませるなら:Newtonsoft.Json を使い続ける
  • System.Text.Json に統一したいなら:JsonDocument / Utf8JsonReader で読み、DataTable を手で組み立てる
  • 呼び出し側の記述を揃えたいなら:JsonConverter<DataTable> を自作して Deserialize に組み込む

.NET Framework 4.7.2 で System.Text.Json を使う前提

System.Text.Json は .NET(.NET Core / .NET)標準の JSON ライブラリとして知られていますが、.NET Framework 4.7.2 では NuGet 参照で利用する形になります。最低限、次のように追加しておくと話が早いです(プロジェクトの状況により依存関係は変わります)。

Install-Package System.Text.Json

また DataTable は System.Data 名前空間にあるため、参照と using を忘れないようにします。

まず押さえる:JSON と DataTable が噛み合いにくい理由

System.Text.Json が DataTable を“そのまま”扱いにくいのは、単に「サポートしていないから」だけではありません。DataTable 自体が、JSON の性質とズレやすい構造を持っていることが本質です。

観点JSONDataTable結果として起きやすいこと
スキーマ(列定義)基本的にスキーマレス列名・型が明確列名の収集・列型の推論が必要
null の表現nullDBNull.Valuenull を入れると例外や不整合になりやすい
数値型整数/小数の区別はあるが型幅は曖昧int/long/decimal/double など厳格列型が安定せず、途中で変換失敗しやすい
ネスト(オブジェクト/配列)普通に入るセルは基本的にスカラ(単一値)想定ネストは文字列化するか、別テーブル化など設計が必要

つまり「JSON を読み取って DataTable を作る」には、列名を集める、列型を決める(または object にする)、null を DBNull に変える、ネストをどう扱うか決めるといった“設計判断”が必要になります。Newtonsoft.Json はこの辺りを便利に肩代わりしてくれる場面が多い、というだけです。

再現:JsonSerializer.Deserialize<DataTable> を試すとどうなるか

.NET Framework 4.7.2 で System.Text.Json(NuGet)を参照し、次のように書きたくなるケースがよくあります。

using System;
using System.Data;
using System.Text.Json;

public class Demo
{
    public static void Main()
    {
        string json = "[{\\"Id\\":1,\\"Name\\":\\"Alice\\"},{\\"Id\\":2,\\"Name\\":\\"Bob\\"}]";

        var options = new JsonSerializerOptions
        {
            PropertyNameCaseInsensitive = true
        };

        // これがそのまま動けば理想
        DataTable table = JsonSerializer.Deserialize<DataTable>(json, options);

        Console.WriteLine(table.Rows.Count);
    }
}

しかし実際には、実行時に NotSupportedException などが発生して止まることがあります。例外メッセージの雰囲気としては「DataTable のシリアライズ/デシリアライズはサポートされない」といった内容です(System.Text.Json のバージョンにより文言は多少変わります)。

ここで重要なのは、「オプションを工夫すれば解決する」類の問題ではないことです。DataTable を扱うための変換ロジック(コンバーター)が用意されていないため、呼び出し側が自分でルールを決めて実装する必要があります。

解決策を比較して選ぶ

同じ「JSON → 表形式データ」でも、求めるゴールによって最適解は変わります。実務で迷いやすいポイントを、先に表で整理しておきます。

方針実装コスト変換の柔軟性保守性おすすめ場面
Newtonsoft.Json を使う低高高(実績が多い)とにかく早く DataTable が欲しい/既存資産が Newtonsoft 前提
System.Text.Json で手組み(JsonDocument など)中高(要件に合わせて作れる)中(自前コードを育てる必要)ライブラリ統一/変換ルールを明確にしたい/ネスト処理も含め制御したい
System.Text.Json + JsonConverter 自作中〜高高中(Converter のテストが必須)呼び出し側を Deserialize<DataTable>() で統一したい
DataTable をやめて List<T> へ寄せる中(設計変更)中〜高高(型安全)新規開発/データの形が決まっている/将来の保守性を優先

解決策:Newtonsoft.Json を使い続ける(最短・堅実)

「System.Text.Json を使いたい」という気持ちは分かりますが、JSON → DataTable を“手早く・確実に”やりたいなら、Newtonsoft.Json を選ぶのが現実的です。DataTable 変換は利用実績が非常に多く、ハマりどころも共有され尽くしています。

using System.Data;
using Newtonsoft.Json;

// JSON が「オブジェクト配列」の形なら、そのまま DataTable にできることが多い
DataTable table = JsonConvert.DeserializeObject<DataTable>(json);

特に既存システムが DataTable を中心に組まれている場合、ここで無理に System.Text.Json に寄せるよりも、変換部分だけ Newtonsoft.Json を許容して全体最適を取る方が、結果的にバグも工数も減りやすいです。

解決策:System.Text.Json で DataTable を手で組み立てる(実務で一番コントロールしやすい)

「Microsoft 提供の System.Text.Json に統一したい」「変換ルールを自分たちの要件に合わせたい」という場合は、JsonDocument で JSON を読み取り、DataTable を自前で構築するのが分かりやすいです。

前提:JSON は“配列+フラットなオブジェクト”を想定する

DataTable に素直に落とし込める JSON は、次のような形です。

[
  { "Id": 1, "Name": "Alice", "Active": true, "Created": "2025-12-29T10:00:00Z" },
  { "Id": 2, "Name": "Bob",   "Active": false, "Created": null }
]

配列の各要素が 1 行(DataRow)、オブジェクトの各プロパティが列(DataColumn)に対応します。ネスト(子オブジェクト/子配列)がある場合は、後述のルール決めが必要です。

実装例:列型を object にして安全側に倒す

列型推論を頑張るほど複雑になります。まずはすべての列を typeof(object) にして、値だけ “それっぽい .NET 型” に変換するやり方が、実務では事故が少なめです(フィルタや集計を DataTable 側で多用するなら、後で列型を最適化する設計も可能です)。

さらに重要なのが、列名を大小文字を無視して扱いたい場合、TryGetProperty() のままだと取りこぼす点です(System.Text.Json のプロパティ検索は基本的に大小文字を区別します)。そこで、行オブジェクトを列マップに照らして埋める方式にすると安定します。

using System;
using System.Collections.Generic;
using System.Data;
using System.Text.Json;

public static class JsonDataTable
{
    public static DataTable FromJsonArray(string json, bool caseInsensitiveColumnName = true)
    {
        if (string.IsNullOrWhiteSpace(json))
            throw new ArgumentException("json is empty.", nameof(json));

        using (JsonDocument doc = JsonDocument.Parse(json))
        {
            return FromJsonArray(doc.RootElement, caseInsensitiveColumnName);
        }
    }

    public static DataTable FromJsonArray(JsonElement root, bool caseInsensitiveColumnName = true)
    {
        if (root.ValueKind != JsonValueKind.Array)
            throw new NotSupportedException("JSON は配列([ ... ])形式を想定しています。");

        // 先に列名を収集(出現順を保持)
        var columns = new List<string>();
        var comparer = caseInsensitiveColumnName ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal;
        var columnSet = new HashSet<string>(comparer);

        foreach (JsonElement rowEl in root.EnumerateArray())
        {
            if (rowEl.ValueKind != JsonValueKind.Object)
                throw new NotSupportedException("配列の各要素はオブジェクト({ ... })である必要があります。");

            foreach (JsonProperty prop in rowEl.EnumerateObject())
            {
                if (columnSet.Add(prop.Name))
                    columns.Add(prop.Name);
            }
        }

        var table = new DataTable();
        table.CaseSensitive = !caseInsensitiveColumnName;

        foreach (string name in columns)
            table.Columns.Add(name, typeof(object));

        // 列名 → 列位置 のマップ(大小文字の扱いをそろえる)
        var colIndex = new Dictionary<string, int>(comparer);
        for (int i = 0; i < columns.Count; i++)
            colIndex[columns[i]] = i;

        // 行を追加(欠損列は DBNull、存在する列だけ埋める)
        foreach (JsonElement rowEl in root.EnumerateArray())
        {
            DataRow row = table.NewRow();

            // いったん全部 DBNull にしてから、存在するプロパティだけ上書きする
            for (int i = 0; i < columns.Count; i++)
                row[i] = DBNull.Value;

            foreach (JsonProperty prop in rowEl.EnumerateObject())
            {
                int idx;
                if (colIndex.TryGetValue(prop.Name, out idx))
                    row[idx] = ToCellValue(prop.Value);
            }

            table.Rows.Add(row);
        }

        return table;
    }

    private static object ToCellValue(JsonElement el)
    {
        switch (el.ValueKind)
        {
            case JsonValueKind.String:
                // DateTime に寄せたい場合だけ TryGetDateTime を使う
                DateTime dt;
                if (el.TryGetDateTime(out dt))
                    return dt;

                return el.GetString();

            case JsonValueKind.Number:
                long l;
                if (el.TryGetInt64(out l))
                    return l;

                decimal dec;
                if (el.TryGetDecimal(out dec))
                    return dec;

                return el.GetDouble();

            case JsonValueKind.True:
                return true;

            case JsonValueKind.False:
                return false;

            case JsonValueKind.Null:
            case JsonValueKind.Undefined:
                return DBNull.Value;

            case JsonValueKind.Object:
            case JsonValueKind.Array:
                // ネストはセルに直接入れにくいので JSON 文字列として保持
                return el.GetRawText();

            default:
                return el.GetRawText();
        }
    }
}

使い方はシンプルです。

DataTable table = JsonDataTable.FromJsonArray(json);

// DataGridView にバインドする例
dataGridView1.DataSource = table;

JSON がオブジェクトに包まれている場合(items プロパティなど)

API レスポンスでよくあるのが、配列が 1 段深い場所にあるパターンです。

{
  "items": [
    { "Id": 1, "Name": "Alice" },
    { "Id": 2, "Name": "Bob" }
  ]
}

この場合は、いったん配列要素を抜き出してから変換します。

using (JsonDocument doc = JsonDocument.Parse(json))
{
    JsonElement array = doc.RootElement.GetProperty("items");
    DataTable table = JsonDataTable.FromJsonArray(array);
}

JsonValueKind と DataTable への入れ方の“落としどころ”

上の実装が採用している変換ルールを、表にすると次のイメージです。「何をどの型にするか」はプロジェクトごとに最適解が変わるので、ここを基準に自社ルールへ調整していくのがおすすめです。

JsonValueKind例DataTable のセルに入れる値(例)注意点
String“Alice”string(必要なら DateTime)日付文字列を DateTime にすると表示は楽だが、パース条件を明確に
Number1 / 1.23long / decimal / double列内で型が揺れると後工程でつらい。まずは decimal or string に寄せるのも手
True/Falsetruebool特になし
NullnullDBNull.ValueDataTable は null ではなく DBNull が基本
Object/Array{…} / […]Raw JSON 文字列(GetRawText())“セルに入れる”なら文字列化が安全。集計したいなら別設計が必要

実務の工夫:スキーマが分かっているなら先に列を定義する

入力 JSON の形が決まっている(たとえば API のレスポンス仕様が固定)なら、列名と列型を先に決めてしまう方が事故が減ります。型推論が不要になり、列内の型ブレも防げます。

var table = new DataTable();
table.Columns.Add("Id", typeof(int));
table.Columns.Add("Name", typeof(string));
table.Columns.Add("Created", typeof(DateTime)); // 運用次第で DateTime? や string でもOK

// 以降は JSON を読み取り、列に合わせて値を詰める(null は DBNull)

「型が決まっているのに object 列で持つ」よりも、SQL への投入(SqlBulkCopy など)やフィルタが絡む処理では、この方式が安定します。

解決策:JsonConverter<DataTable> を自作して Deserialize に組み込む

呼び出し側のコードを JsonSerializer.Deserialize<DataTable>(json, options) に統一したい場合は、JsonConverter<DataTable> を作って options に登録します。ポイントは「実装を頑張りすぎない」ことです。Converter 内部で JsonDocument に落としてから先ほどの手組み関数を呼ぶ形にすると、読み取りロジックを 1 箇所に集約できます。

using System;
using System.Data;
using System.Text.Json;
using System.Text.Json.Serialization;

public sealed class DataTableJsonConverter : JsonConverter<DataTable>
{
    public override DataTable Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        // 現在位置の JSON 値(配列やオブジェクトなど)を丸ごと取り込み
        using (JsonDocument doc = JsonDocument.ParseValue(ref reader))
        {
            // 配列を前提(必要ならここで "items" プロパティを抜き出す等を実装)
            return JsonDataTable.FromJsonArray(doc.RootElement);
        }
    }

    public override void Write(Utf8JsonWriter writer, DataTable value, JsonSerializerOptions options)
    {
        writer.WriteStartArray();

        foreach (DataRow row in value.Rows)
        {
            writer.WriteStartObject();

            foreach (DataColumn col in value.Columns)
            {
                writer.WritePropertyName(col.ColumnName);

                object cell = row[col];
                if (cell == null || cell == DBNull.Value)
                {
                    writer.WriteNullValue();
                }
                else
                {
                    // 値の型に応じて System.Text.Json に委譲
                    JsonSerializer.Serialize(writer, cell, cell.GetType(), options);
                }
            }

            writer.WriteEndObject();
        }

        writer.WriteEndArray();
    }
}

登録して使います。

var options = new JsonSerializerOptions
{
    PropertyNameCaseInsensitive = true
};
options.Converters.Add(new DataTableJsonConverter());

DataTable table = JsonSerializer.Deserialize<DataTable>(json, options);

Converter 方式のメリットは、呼び出し側がすっきりすることです。一方で、変換ルールは結局自分たちで決める必要があります。特に次の点は、プロジェクトに合わせて調整が必要です。

  • 「ネストした JSON をどう扱うか」(文字列化、別テーブル化、フラット化…)
  • 列型は object で統一するか、推論して型付きにするか
  • 日時文字列を DateTime にするか、文字列のままにするか
  • 列名の大文字小文字をどう扱うか(Id と id を同一視するか)

補足:List<Dictionary<string, object>> や POCO に寄せてから DataTable を作る

JSON が「オブジェクト配列」なら、System.Text.Json の得意領域である List<T> へ落としてから DataTable を作る方法も有効です。DataTable への変換処理を自前で持つ点は同じですが、JSON の解釈は System.Text.Json に任せられます。

Dictionary 経由(列が可変なデータ向け)

列の増減が多いデータ(ログや外部連携データ)では、POCO より Dictionary の方が相性が良い場合があります。ただし、object に入るのは JsonElement になることが多いので、結局は JsonElement → .NET 型の変換が必要になります。

using System.Collections.Generic;
using System.Text.Json;

List<Dictionary<string, JsonElement>> rows =
    JsonSerializer.Deserialize<List<Dictionary<string, JsonElement>>>(json);

POCO 経由(仕様が固定なデータ向け)

仕様が固定なら、POCO にするのが一番保守しやすいです。DataTable を使うのが「既存 API の都合」「UI コンポーネントの都合」だけなら、データ保持は List<T>、最後に DataTable を作る、という分離ができます。

public sealed class PersonRow
{
    public int Id { get; set; }
    public string Name { get; set; }
    public bool Active { get; set; }
    public DateTime? Created { get; set; }
}

var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
List<PersonRow> list = JsonSerializer.Deserialize<List<PersonRow>>(json, options);

POCO から DataTable を作る汎用メソッドを用意しておくと、レガシー連携でも運用が楽になります。

using System;
using System.Collections.Generic;
using System.Data;
using System.Reflection;

public static class DataTableExtensions
{
    public static DataTable ToDataTable<T>(this IEnumerable<T> items)
    {
        var table = new DataTable(typeof(T).Name);

        PropertyInfo[] props = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);

        foreach (var p in props)
        {
            Type colType = Nullable.GetUnderlyingType(p.PropertyType) ?? p.PropertyType;
            table.Columns.Add(p.Name, colType);
        }

        foreach (var item in items)
        {
            DataRow row = table.NewRow();

            foreach (var p in props)
            {
                object val = p.GetValue(item, null);
                row[p.Name] = val ?? DBNull.Value;
            }

            table.Rows.Add(row);
        }

        return table;
    }
}

よくある落とし穴と、事前に決めておくべきルール

System.Text.Json で「JSON → DataTable」をやるときに、現場で特にトラブルになりやすい点をまとめます。ここを先に合意しておくと、Converter や手組み実装がブレにくくなります。

列名の揺れ(Id と id)

JSON のプロパティ名は大小文字が区別されますが、DataTable は既定で大小文字を区別しない運用になりがちです。外部データで列名が揺れる可能性があるなら、次のどちらかに寄せるのが安全です。

  • 最初から列名を小文字に正規化して扱う(列名変換マップを持つ)
  • 大小文字を無視して同じ列にまとめる(Converter 側で比較を統一する)

欠損列の扱い(行によってキーが無い)

配列の各行で持っているキーが違う場合、DataTable の列を「全行の和集合」にするか「最初の行に合わせるか」で結果が変わります。実務では、次のいずれかが多いです。

  • 和集合方式:存在しない列は DBNull(例:ログや疎なデータ)
  • 固定方式:仕様にない列は無視(例:API 仕様が固定)

数値の型(int/long/decimal/double)

JSON の数値は、入力の大きさや小数の有無で扱いが変わります。列型推論を入れる場合は、少なくとも「列内で小数が出たら decimal に寄せる」「オーバーフローしそうなら string に寄せる」など、例外が出ない方向に寄せるのがコツです。

入力例推奨の寄せ方(例)理由
1, 2, 3longint だと上限に当たる可能性があるため
1.2, 3.4decimaldouble は二進浮動小数で誤差が出やすい
非常に大きい数、桁数が多いstring金融・ID・コード類は数値にしない方が安全なことが多い

日時文字列を DateTime にするか

TryGetDateTime は便利ですが、日時として解釈して良い文字列だけを DateTime にするという前提が必要です。例えば「20250101」のようなコードを日時に変換してしまうと事故になります。実務では次のようなルールがよく使われます。

  • 列名が CreatedAt/UpdatedAt/Date のように日時を示す場合だけ DateTime に変換
  • それ以外の文字列は原則 string のまま(見た目が日時でも変換しない)

ネスト JSON の扱い

DataTable の 1 セルに子オブジェクトを入れたい場合、最も安全なのは Raw JSON 文字列として保持することです。表示やログ保存には十分ですが、集計や検索をしたいなら次のような設計が必要になります。

  • 子要素をフラット化して列に展開(例:Address.City → Address_City)
  • 親テーブルと子テーブルに分割して DataSet 化
  • DataTable ではなく、JSON を扱える別のデータ構造で保持

「System.Text.Json に統一すべきか?」の現実的な判断基準

System.Text.Json は高速・省メモリで、標準ライブラリとしての安心感もあります。一方で DataTable のような ADO.NET 系の“便利型”は、Newtonsoft.Json の方が得意です。実務では、次のように割り切ると判断が楽になります。

  • DataTable が必要な範囲だけ Newtonsoft.Json(変換レイヤーで隔離)
  • それ以外の JSON 処理は System.Text.Json に統一(新規 API など)
  • 可能なら中長期で DataTable 依存を減らし List<T> に移行(型安全・テスト容易)

「ライブラリ統一」を目的に変換の複雑さが増え、バグが増えるのは本末転倒です。DataTable は強力ですが、スキーマレスな JSON と組み合わせると設計判断が増えます。要件(速度、保守性、既存資産、変換ルールの明確さ)を軸に選ぶのが一番現実的です。

まとめ:System.Text.Json で DataTable を扱うなら“変換ルール”が肝

System.Text.Json で JSON を DataTable に直接デシリアライズするのは、標準機能だけでは難しいのが実情です。やるなら、手組み実装か JsonConverter 自作が必要になります。一方で、短期で確実に進めたいなら Newtonsoft.Json を使い続けるのが堅実です。

特に現場で失敗を減らすコツは、次の 3 点です。

  • 入力 JSON の形(配列・フラット・ネスト有無)を前提として固定する
  • 列型推論を欲張らず、まずは object 列+DBNull で安全に動かす
  • 日時・数値・ネストの扱いを “自社ルール” として明文化し、テストを書く

これらを押さえれば、System.Text.Json でも「JSON → DataTable」を十分実務投入できます。逆に言えば、ルールが曖昧なまま Converter を作ると、データが増えた瞬間に破綻しやすい領域でもあります。早めに設計判断を固めて進めましょう。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次