ファイル

【C#】JSONとDictionaryを高速かつ柔軟に相互変換する方法

JSONとDictionaryの相互変換はSystem.Text.Json.JsonSerializerNewtonsoft.Json.JsonConvertを使えば一行で済みます。

文字列キーなら追加設定は不要、複雑な型はカスタムコンバーターで対応できます。

パフォーマンス重視なら前者、互換性や機能重視なら後者が便利です。

JSONとDictionaryの基本

なぜ相互変換が重要か

C#で開発を進める際、JSONとDictionaryの相互変換は非常に重要な役割を果たします。

JSONは軽量で人間にも読みやすいデータフォーマットとして、Web APIの通信や設定ファイル、データ保存など幅広い用途で使われています。

一方、DictionaryはC#の標準的なコレクションで、キーと値のペアを効率的に管理できるため、プログラム内でのデータ操作に適しています。

この2つの形式をスムーズに変換できることは、外部から受け取ったJSONデータをプログラム内で扱いやすい形に変換したり、逆にプログラム内のデータをJSON形式で外部に送信したりする際に欠かせません。

例えば、REST APIのレスポンスをDictionaryに変換して処理したり、ユーザー設定をDictionaryで管理しJSONファイルに保存したりするケースが典型的です。

また、Dictionaryはキーが文字列であることが多いため、JSONのオブジェクト構造と親和性が高く、相互変換が直感的に行えます。

これにより、データの柔軟な操作や拡張が可能となり、開発効率の向上にもつながります。

Dictionaryシリアライズの前提知識

DictionaryをJSONにシリアライズする際には、いくつかの前提知識を押さえておくとスムーズに実装できます。

まず、Dictionaryのキーは基本的に文字列型であることが望ましいです。

JSONのオブジェクトはキーが文字列であるため、Dictionary<string, TValue>の形が最も自然にマッピングされます。

もしキーが文字列以外の型(例えば整数や列挙型)である場合、標準のシリアライズ処理ではエラーが発生したり、意図しない結果になることがあります。

この場合はカスタムコンバーターの実装が必要です。

次に、値の型についても注意が必要です。

値がプリミティブ型(文字列、数値、真偽値など)であれば問題ありませんが、複雑なオブジェクトやネストしたコレクションを値に持つ場合は、シリアライズ時に適切な型情報が保持されるように設定を調整する必要があります。

また、シリアライズ時のオプション設定も重要です。

例えば、null値の扱いやインデントの有無、プロパティ名の変換ルールなどを適切に設定することで、出力されるJSONの可読性や互換性を高められます。

最後に、デシリアライズ時にはJSONの構造がDictionaryの型に合致しているかを確認することが大切です。

JSONの形式が期待と異なる場合、例外が発生したり、データが正しく読み込めなかったりするため、エラーハンドリングも考慮しましょう。

JSONライブラリ選定のポイント

C#でJSONとDictionaryの相互変換を行う際に使われる代表的なライブラリは、System.Text.JsonNewtonsoft.Json(Json.NET)です。

どちらを選ぶかはプロジェクトの要件や開発環境によって異なりますが、選定のポイントを理解しておくと適切な判断ができます。

パフォーマンス

System.Text.Jsonは.NET Core 3.0以降で標準搭載されており、パフォーマンスに優れています。

特に高速なシリアライズ・デシリアライズが求められる場面で効果を発揮します。

一方、Newtonsoft.Jsonは機能が豊富ですが、やや処理速度が劣る場合があります。

機能の豊富さと柔軟性

Newtonsoft.Jsonは長年にわたり多くの機能が追加されており、複雑なシナリオやカスタム変換、柔軟な設定が可能です。

例えば、非文字列キーのDictionaryのシリアライズや、複雑な型の変換、詳細なエラーハンドリングなどに強みがあります。

System.Text.Jsonもカスタムコンバーターの実装が可能ですが、現時点ではNewtonsoft.Jsonほど多機能ではありません。

ただし、バージョンアップにより機能は拡充されています。

互換性とエコシステム

Newtonsoft.Jsonは多くの既存プロジェクトやライブラリで採用されており、互換性の面で安心感があります。

特に古い.NET Framework環境や、外部ライブラリとの連携が必要な場合に有利です。

System.Text.Jsonは最新の.NET環境に最適化されており、将来的なメンテナンス性を考慮すると推奨されるケースが増えています。

使いやすさと学習コスト

System.Text.Jsonは標準ライブラリのため、追加のパッケージインストールが不要で、APIもシンプルです。

初学者でも扱いやすい設計となっています。

Newtonsoft.Jsonは機能が多いため学習コストはやや高いですが、ドキュメントやサンプルが豊富で、コミュニティのサポートも充実しています。

これらのポイントを踏まえ、プロジェクトの要件や開発環境に合わせて適切なJSONライブラリを選択し、Dictionaryとの相互変換を実装すると良いでしょう。

System.Text.Jsonを使った変換

基本的なシリアライズ手順

文字列キーと値のDictionary

System.Text.Jsonを使って、Dictionary<string, string>をJSON文字列にシリアライズするのは非常にシンプルです。

JsonSerializer.SerializeメソッドにDictionaryを渡すだけで、キーと値のペアがJSONオブジェクトとして出力されます。

以下のサンプルコードでは、文字列キーと文字列値のDictionaryを作成し、JSONに変換しています。

using System;
using System.Collections.Generic;
using System.Text.Json;
class Program
{
    static void Main()
    {
        // 文字列キーと値のDictionaryを作成
        var dictionary = new Dictionary<string, string>
        {
            { "name", "Alice" },
            { "city", "Tokyo" },
            { "language", "C#" }
        };
        // DictionaryをJSON文字列にシリアライズ
        string jsonString = JsonSerializer.Serialize(dictionary);
        Console.WriteLine(jsonString);
    }
}
{"name":"Alice","city":"Tokyo","language":"C#"}

このように、キーが文字列であれば、特別な設定なしに簡単にJSONに変換できます。

ネストしたオブジェクトやコレクション

Dictionaryの値にさらにオブジェクトやリストを含める場合も、System.Text.Jsonは自動的にネスト構造をシリアライズします。

例えば、Dictionary<string, object>の値に別のDictionaryやリストを入れても問題ありません。

以下の例では、値にネストしたDictionaryとリストを含めています。

using System;
using System.Collections.Generic;
using System.Text.Json;
class Program
{
    static void Main()
    {
        var nestedDict = new Dictionary<string, object>
        {
            { "id", 123 },
            { "tags", new List<string> { "json", "csharp", "dictionary" } },
            { "details", new Dictionary<string, string>
                {
                    { "author", "Bob" },
                    { "version", "1.0" }
                }
            }
        };
        string jsonString = JsonSerializer.Serialize(nestedDict, new JsonSerializerOptions { WriteIndented = true });
        Console.WriteLine(jsonString);
    }
}
{
  "id": 123,
  "tags": [
    "json",
    "csharp",
    "dictionary"
  ],
  "details": {
    "author": "Bob",
    "version": "1.0"
  }
}

このように、複雑なネスト構造も自然にJSONに変換されます。

基本的なデシリアライズ手順

静的型指定でのマッピング

JSON文字列をDictionary<string, string>Dictionary<string, object>に変換する場合、JsonSerializer.Deserialize<T>に型を指定して呼び出します。

型が明確な場合は静的型指定が最も簡単で安全です。

以下は、JSON文字列をDictionary<string, string>にデシリアライズする例です。

using System;
using System.Collections.Generic;
using System.Text.Json;
class Program
{
    static void Main()
    {
        string jsonString = "{\"name\":\"Alice\",\"city\":\"Tokyo\",\"language\":\"C#\"}";
        var dictionary = JsonSerializer.Deserialize<Dictionary<string, string>>(jsonString);
        foreach (var kvp in dictionary)
        {
            Console.WriteLine($"Key: {kvp.Key}, Value: {kvp.Value}");
        }
    }
}
Key: name, Value: Alice
Key: city, Value: Tokyo
Key: language, Value: C#

型を指定することで、JSONの各値が指定した型に変換され、扱いやすくなります。

JsonElementを用いた動的パース

JSONの構造が不明確だったり、動的に解析したい場合は、Dictionary<string, JsonElement>を使う方法があります。

JsonElementはJSONの任意の値を表現でき、後から型を判別して処理できます。

以下の例では、JsonElementを使って値の型を判別しながら出力しています。

using System;
using System.Collections.Generic;
using System.Text.Json;
class Program
{
    static void Main()
    {
        string jsonString = "{\"name\":\"Alice\",\"age\":30,\"isMember\":true}";
        var dictionary = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(jsonString);
        foreach (var kvp in dictionary)
        {
            Console.Write($"Key: {kvp.Key}, Value: ");
            switch (kvp.Value.ValueKind)
            {
                case JsonValueKind.String:
                    Console.WriteLine(kvp.Value.GetString());
                    break;
                case JsonValueKind.Number:
                    Console.WriteLine(kvp.Value.GetInt32());
                    break;
                case JsonValueKind.True:
                case JsonValueKind.False:
                    Console.WriteLine(kvp.Value.GetBoolean());
                    break;
                default:
                    Console.WriteLine("Unsupported type");
                    break;
            }
        }
    }
}
Key: name, Value: Alice
Key: age, Value: 30
Key: isMember, Value: True

この方法は柔軟ですが、値の型判定や変換処理を自分で実装する必要があります。

オプション設定と挙動

PropertyNamingPolicy

JsonSerializerOptionsPropertyNamingPolicyを設定すると、シリアライズ時のプロパティ名の変換ルールを指定できます。

例えば、キャメルケースに変換したい場合はJsonNamingPolicy.CamelCaseを指定します。

using System;
using System.Collections.Generic;
using System.Text.Json;
class Program
{
    static void Main()
    {
        var dictionary = new Dictionary<string, string>
        {
            { "FirstName", "Alice" },
            { "LastName", "Smith" }
        };
        var options = new JsonSerializerOptions
        {
            PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
            WriteIndented = true
        };
        string jsonString = JsonSerializer.Serialize(dictionary, options);
        Console.WriteLine(jsonString);
    }
}
{
  "firstName": "Alice",
  "lastName": "Smith"
}

ただし、Dictionaryのキーは元々文字列なので、PropertyNamingPolicyはクラスのプロパティ名に対して効果があります。

Dictionaryのキーには影響しないため、キー名を変換したい場合は別途変換処理が必要です。

IgnoreNullValues

IgnoreNullValuesオプションをtrueに設定すると、値がnullのプロパティやDictionaryの要素はシリアライズ時に除外されます。

これにより、不要なnullフィールドを省略してJSONを軽量化できます。

using System;
using System.Collections.Generic;
using System.Text.Json;
class Program
{
    static void Main()
    {
        var dictionary = new Dictionary<string, string?>
        {
            { "name", "Alice" },
            { "nickname", null }
        };
        var options = new JsonSerializerOptions
        {
            IgnoreNullValues = true
        };
        string jsonString = JsonSerializer.Serialize(dictionary, options);
        Console.WriteLine(jsonString);
    }
}
{"name":"Alice"}

nicknameキーは値がnullなのでJSONに含まれていません。

WriteIndented

WriteIndentedtrueに設定すると、出力されるJSONがインデント付きの整形済みになります。

可読性を高めたい場合に便利です。

using System;
using System.Collections.Generic;
using System.Text.Json;
class Program
{
    static void Main()
    {
        var dictionary = new Dictionary<string, string>
        {
            { "name", "Alice" },
            { "city", "Tokyo" }
        };
        var options = new JsonSerializerOptions
        {
            WriteIndented = true
        };
        string jsonString = JsonSerializer.Serialize(dictionary, options);
        Console.WriteLine(jsonString);
    }
}
{
  "name": "Alice",
  "city": "Tokyo"
}

カスタムコンバーター実装例

Enumキー対応

System.Text.JsonはデフォルトでDictionaryのキーに文字列以外の型を使うと例外が発生します。

特にenumをキーにしたい場合は、カスタムコンバーターを実装して文字列に変換する必要があります。

以下は、enumをキーに持つDictionaryをJSONにシリアライズ・デシリアライズするためのカスタムコンバーターの例です。

using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
enum Status
{
    Active,
    Inactive,
    Pending
}
class EnumKeyDictionaryConverter<TValue> : JsonConverter<Dictionary<Status, TValue>>
{
    public override Dictionary<Status, TValue> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        var dict = new Dictionary<Status, TValue>();
        if (reader.TokenType != JsonTokenType.StartObject)
            throw new JsonException();
        while (reader.Read())
        {
            if (reader.TokenType == JsonTokenType.EndObject)
                return dict;
            string keyString = reader.GetString();
            if (!Enum.TryParse<Status>(keyString, out var key))
                throw new JsonException($"Invalid enum key: {keyString}");
            reader.Read();
            TValue value = JsonSerializer.Deserialize<TValue>(ref reader, options);
            dict.Add(key, value);
        }
        throw new JsonException();
    }
    public override void Write(Utf8JsonWriter writer, Dictionary<Status, TValue> value, JsonSerializerOptions options)
    {
        writer.WriteStartObject();
        foreach (var kvp in value)
        {
            writer.WritePropertyName(kvp.Key.ToString());
            JsonSerializer.Serialize(writer, kvp.Value, options);
        }
        writer.WriteEndObject();
    }
}
class Program
{
    static void Main()
    {
        var dict = new Dictionary<Status, string>
        {
            { Status.Active, "User is active" },
            { Status.Inactive, "User is inactive" }
        };
        var options = new JsonSerializerOptions();
        options.Converters.Add(new EnumKeyDictionaryConverter<string>());
        string json = JsonSerializer.Serialize(dict, options);
        Console.WriteLine(json);
        var deserialized = JsonSerializer.Deserialize<Dictionary<Status, string>>(json, options);
        foreach (var kvp in deserialized)
        {
            Console.WriteLine($"{kvp.Key}: {kvp.Value}");
        }
    }
}
{"Active":"User is active","Inactive":"User is inactive"}
Active: User is active
Inactive: User is inactive

このように、enumキーを文字列として扱うことで、System.Text.Jsonでも安全に変換できます。

DateTimeフォーマット制御

System.Text.JsonはデフォルトでISO 8601形式の日時文字列を扱いますが、独自のフォーマットでシリアライズ・デシリアライズしたい場合はカスタムコンバーターを作成します。

以下は、DateTimeyyyy/MM/dd形式でシリアライズする例です。

using System;
using System.Text.Json;
using System.Text.Json.Serialization;
class CustomDateTimeConverter : JsonConverter<DateTime>
{
    private const string Format = "yyyy/MM/dd";
    public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        string? dateString = reader.GetString();
        if (DateTime.TryParseExact(dateString, Format, null, System.Globalization.DateTimeStyles.None, out var date))
        {
            return date;
        }
        throw new JsonException($"Invalid date format. Expected {Format}");
    }
    public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
    {
        writer.WriteStringValue(value.ToString(Format));
    }
}
class Program
{
    static void Main()
    {
        var obj = new Dictionary<string, DateTime>
        {
            { "start", new DateTime(2024, 6, 1) },
            { "end", new DateTime(2024, 6, 30) }
        };
        var options = new JsonSerializerOptions();
        options.Converters.Add(new CustomDateTimeConverter());
        options.WriteIndented = true;
        string json = JsonSerializer.Serialize(obj, options);
        Console.WriteLine(json);
        var deserialized = JsonSerializer.Deserialize<Dictionary<string, DateTime>>(json, options);
        foreach (var kvp in deserialized)
        {
            Console.WriteLine($"{kvp.Key}: {kvp.Value:yyyy-MM-dd}");
        }
    }
}
{
  "start": "2024/06/01",
  "end": "2024/06/30"
}
start: 2024-06-01
end: 2024-06-30

このように、日時のフォーマットを自由に制御できます。

非文字列キーDictionaryの処理

System.Text.JsonDictionaryのキーが文字列以外の場合、標準ではシリアライズ・デシリアライズができません。

整数やカスタム型のキーを使いたい場合は、カスタムコンバーターを作成してキーを文字列に変換する必要があります。

以下は、intをキーに持つDictionaryのカスタムコンバーター例です。

using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
class IntKeyDictionaryConverter<TValue> : JsonConverter<Dictionary<int, TValue>>
{
    public override Dictionary<int, TValue> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        var dict = new Dictionary<int, TValue>();
        if (reader.TokenType != JsonTokenType.StartObject)
            throw new JsonException();
        while (reader.Read())
        {
            if (reader.TokenType == JsonTokenType.EndObject)
                return dict;
            string keyString = reader.GetString();
            if (!int.TryParse(keyString, out int key))
                throw new JsonException($"Invalid int key: {keyString}");
            reader.Read();
            TValue value = JsonSerializer.Deserialize<TValue>(ref reader, options);
            dict.Add(key, value);
        }
        throw new JsonException();
    }
    public override void Write(Utf8JsonWriter writer, Dictionary<int, TValue> value, JsonSerializerOptions options)
    {
        writer.WriteStartObject();
        foreach (var kvp in value)
        {
            writer.WritePropertyName(kvp.Key.ToString());
            JsonSerializer.Serialize(writer, kvp.Value, options);
        }
        writer.WriteEndObject();
    }
}
class Program
{
    static void Main()
    {
        var dict = new Dictionary<int, string>
        {
            { 1, "One" },
            { 2, "Two" }
        };
        var options = new JsonSerializerOptions();
        options.Converters.Add(new IntKeyDictionaryConverter<string>());
        string json = JsonSerializer.Serialize(dict, options);
        Console.WriteLine(json);
        var deserialized = JsonSerializer.Deserialize<Dictionary<int, string>>(json, options);
        foreach (var kvp in deserialized)
        {
            Console.WriteLine($"{kvp.Key}: {kvp.Value}");
        }
    }
}
{"1":"One","2":"Two"}
1: One
2: Two

このように、キーを文字列に変換してJSONに出力し、逆に文字列から元の型に戻す処理を実装することで、非文字列キーのDictionaryも扱えます。

Newtonsoft.Jsonを使った変換

基本的なシリアライズ手順

シンプルなDictionary

Newtonsoft.Json(Json.NET)を使ったシンプルなDictionary<string, string>のシリアライズは非常に簡単です。

JsonConvert.SerializeObjectメソッドにDictionaryを渡すだけで、JSON文字列に変換されます。

以下のサンプルコードでは、文字列キーと値のDictionaryをJSONにシリアライズしています。

using System;
using System.Collections.Generic;
using Newtonsoft.Json;
class Program
{
    static void Main()
    {
        var dictionary = new Dictionary<string, string>
        {
            { "name", "Bob" },
            { "country", "USA" },
            { "language", "C#" }
        };
        string jsonString = JsonConvert.SerializeObject(dictionary);
        Console.WriteLine(jsonString);
    }
}
{"name":"Bob","country":"USA","language":"C#"}

このように、シンプルなDictionaryは特別な設定なしにJSONに変換できます。

多層ネストDictionary

Newtonsoft.Jsonは多層にネストしたDictionaryや複雑なオブジェクト構造も問題なくシリアライズできます。

ネストしたDictionaryやリストを値に持つ場合も、再帰的にJSONに変換されます。

以下の例では、ネストしたDictionaryとリストを含むDictionaryをシリアライズしています。

using System;
using System.Collections.Generic;
using Newtonsoft.Json;
class Program
{
    static void Main()
    {
        var nestedDict = new Dictionary<string, object>
        {
            { "id", 456 },
            { "tags", new List<string> { "json", "newtonsoft", "csharp" } },
            { "metadata", new Dictionary<string, string>
                {
                    { "author", "Carol" },
                    { "version", "2.0" }
                }
            }
        };
        string jsonString = JsonConvert.SerializeObject(nestedDict, Formatting.Indented);
        Console.WriteLine(jsonString);
    }
}
{
  "id": 456,
  "tags": [
    "json",
    "newtonsoft",
    "csharp"
  ],
  "metadata": {
    "author": "Carol",
    "version": "2.0"
  }
}

このように、複雑なネスト構造も自然にJSONに変換されます。

基本的なデシリアライズ手順

型推論による自動マッピング

JsonConvert.DeserializeObjectは、型を指定して呼び出すことで、JSON文字列を自動的に指定した型にマッピングします。

Dictionary<string, string>Dictionary<string, object>など、型に応じて適切に変換されます。

以下は、JSON文字列をDictionary<string, string>にデシリアライズする例です。

using System;
using System.Collections.Generic;
using Newtonsoft.Json;
class Program
{
    static void Main()
    {
        string jsonString = "{\"name\":\"Bob\",\"country\":\"USA\",\"language\":\"C#\"}";
        var dictionary = JsonConvert.DeserializeObject<Dictionary<string, string>>(jsonString);
        foreach (var kvp in dictionary)
        {
            Console.WriteLine($"Key: {kvp.Key}, Value: {kvp.Value}");
        }
    }
}
Key: name, Value: Bob
Key: country, Value: USA
Key: language, Value: C#

型を指定することで、JSONの値が指定した型に変換され、扱いやすくなります。

Genericsで型安全を確保

Newtonsoft.JsonはGenericsを活用して型安全なデシリアライズをサポートします。

例えば、Dictionary<string, int>Dictionary<string, List<string>>など、複雑な型も安全に扱えます。

以下は、Dictionary<string, List<string>>をデシリアライズする例です。

using System;
using System.Collections.Generic;
using Newtonsoft.Json;
class Program
{
    static void Main()
    {
        string jsonString = "{\"fruits\":[\"apple\",\"banana\"],\"vegetables\":[\"carrot\",\"lettuce\"]}";
        var dictionary = JsonConvert.DeserializeObject<Dictionary<string, List<string>>>(jsonString);
        foreach (var kvp in dictionary)
        {
            Console.WriteLine($"Category: {kvp.Key}");
            foreach (var item in kvp.Value)
            {
                Console.WriteLine($" - {item}");
            }
        }
    }
}
Category: fruits
 - apple
 - banana
Category: vegetables
 - carrot
 - lettuce

Genericsを使うことで、型の安全性を保ちながら柔軟にデータを扱えます。

柔軟な設定オプション

NullValueHandling

NullValueHandlingオプションを使うと、null値のプロパティやDictionaryの要素をシリアライズ時に含めるかどうかを制御できます。

NullValueHandling.Ignoreを指定すると、nullの値はJSONに含まれません。

using System;
using System.Collections.Generic;
using Newtonsoft.Json;
class Program
{
    static void Main()
    {
        var dictionary = new Dictionary<string, string?>
        {
            { "name", "Bob" },
            { "nickname", null }
        };
        var settings = new JsonSerializerSettings
        {
            NullValueHandling = NullValueHandling.Ignore
        };
        string jsonString = JsonConvert.SerializeObject(dictionary, settings);
        Console.WriteLine(jsonString);
    }
}
{"name":"Bob"}

nicknameキーは値がnullなのでJSONに含まれていません。

DefaultValueHandling

DefaultValueHandlingは、プロパティのデフォルト値をシリアライズに含めるかどうかを制御します。

例えば、DefaultValueHandling.Ignoreを指定すると、デフォルト値のプロパティはJSONに含まれません。

以下は、デフォルト値を持つDictionaryの例です。

using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using System.ComponentModel;
class Program
{
    static void Main()
    {
        var dictionary = new Dictionary<string, int>
        {
            { "count", 0 },
            { "max", 10 }
        };
        var settings = new JsonSerializerSettings
        {
            DefaultValueHandling = DefaultValueHandling.Ignore
        };
        string jsonString = JsonConvert.SerializeObject(dictionary, settings);
        Console.WriteLine(jsonString);
    }
}
{"max":10}

countの値は0(intのデフォルト値)なのでJSONに含まれていません。

Formatting

Formattingオプションを使うと、出力されるJSONの整形を制御できます。

Formatting.Indentedを指定すると、インデント付きの見やすいJSONが生成されます。

using System;
using System.Collections.Generic;
using Newtonsoft.Json;
class Program
{
    static void Main()
    {
        var dictionary = new Dictionary<string, string>
        {
            { "name", "Bob" },
            { "city", "New York" }
        };
        string jsonString = JsonConvert.SerializeObject(dictionary, Formatting.Indented);
        Console.WriteLine(jsonString);
    }
}
{
  "name": "Bob",
  "city": "New York"
}

カスタムコンバーター実装例

非文字列キーDictionary

Newtonsoft.JsonDictionaryのキーが文字列以外でもシリアライズ・デシリアライズが可能ですが、キーの変換方法をカスタマイズしたい場合はカスタムコンバーターを実装します。

以下は、intをキーに持つDictionaryのカスタムコンバーター例です。

using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
class IntKeyDictionaryConverter<TValue> : JsonConverter<Dictionary<int, TValue>>
{
    public override Dictionary<int, TValue> ReadJson(JsonReader reader, Type objectType, Dictionary<int, TValue>? existingValue, bool hasExistingValue, JsonSerializer serializer)
    {
        var dict = new Dictionary<int, TValue>();
        var jObject = JObject.Load(reader);
        foreach (var property in jObject.Properties())
        {
            if (int.TryParse(property.Name, out int key))
            {
                TValue value = property.Value.ToObject<TValue>(serializer)!;
                dict.Add(key, value);
            }
            else
            {
                throw new JsonSerializationException($"Invalid int key: {property.Name}");
            }
        }
        return dict;
    }
    public override void WriteJson(JsonWriter writer, Dictionary<int, TValue> value, JsonSerializer serializer)
    {
        writer.WriteStartObject();
        foreach (var kvp in value)
        {
            writer.WritePropertyName(kvp.Key.ToString());
            serializer.Serialize(writer, kvp.Value);
        }
        writer.WriteEndObject();
    }
}
class Program
{
    static void Main()
    {
        var dict = new Dictionary<int, string>
        {
            { 10, "Ten" },
            { 20, "Twenty" }
        };
        var settings = new JsonSerializerSettings();
        settings.Converters.Add(new IntKeyDictionaryConverter<string>());
        string json = JsonConvert.SerializeObject(dict, settings);
        Console.WriteLine(json);
        var deserialized = JsonConvert.DeserializeObject<Dictionary<int, string>>(json, settings);
        foreach (var kvp in deserialized!)
        {
            Console.WriteLine($"{kvp.Key}: {kvp.Value}");
        }
    }
}
{"10":"Ten","20":"Twenty"}
10: Ten
20: Twenty

このように、キーの変換をカスタマイズして非文字列キーのDictionaryを扱えます。

カスタムDateTimeフォーマット

Newtonsoft.Jsonでは、JsonConverterを継承してDateTimeのシリアライズ・デシリアライズのフォーマットを自由に制御できます。

以下は、DateTimedd-MM-yyyy形式でシリアライズするカスタムコンバーターの例です。

using System;
using Newtonsoft.Json;
using System.Globalization;
class CustomDateTimeConverter : JsonConverter<DateTime>
{
    private const string Format = "dd-MM-yyyy";
    public override void WriteJson(JsonWriter writer, DateTime value, JsonSerializer serializer)
    {
        writer.WriteValue(value.ToString(Format));
    }
    public override DateTime ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
    {
        string? dateString = reader.Value?.ToString();
        if (DateTime.TryParseExact(dateString, Format, CultureInfo.InvariantCulture, DateTimeStyles.None, out var date))
        {
            return date;
        }
        throw new JsonSerializationException($"Invalid date format. Expected {Format}");
    }
}
class Program
{
    static void Main()
    {
        var obj = new Dictionary<string, DateTime>
        {
            { "start", new DateTime(2024, 6, 1) },
            { "end", new DateTime(2024, 6, 30) }
        };
        var settings = new JsonSerializerSettings();
        settings.Converters.Add(new CustomDateTimeConverter());
        settings.Formatting = Formatting.Indented;
        string json = JsonConvert.SerializeObject(obj, settings);
        Console.WriteLine(json);
        var deserialized = JsonConvert.DeserializeObject<Dictionary<string, DateTime>>(json, settings);
        foreach (var kvp in deserialized!)
        {
            Console.WriteLine($"{kvp.Key}: {kvp.Value:yyyy-MM-dd}");
        }
    }
}
{
  "start": "01-06-2024",
  "end": "30-06-2024"
}
start: 2024-06-01
end: 2024-06-30

バージョン互換性維持

Newtonsoft.Jsonのカスタムコンバーターは、バージョンアップや仕様変更に伴う互換性維持にも役立ちます。

例えば、過去のJSONフォーマットと新しいフォーマットの両方を扱う場合、カスタムコンバーター内で条件分岐して対応可能です。

以下は、旧フォーマットyyyyMMddと新フォーマットyyyy-MM-ddの両方を受け入れる例です。

using System;
using Newtonsoft.Json;
using System.Globalization;
class FlexibleDateTimeConverter : JsonConverter<DateTime>
{
    private readonly string[] formats = { "yyyy-MM-dd", "yyyyMMdd" };
    public override void WriteJson(JsonWriter writer, DateTime value, JsonSerializer serializer)
    {
        writer.WriteValue(value.ToString("yyyy-MM-dd"));
    }
    public override DateTime ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
    {
        string? dateString = reader.Value?.ToString();
        if (DateTime.TryParseExact(dateString, formats, CultureInfo.InvariantCulture, DateTimeStyles.None, out var date))
        {
            return date;
        }
        throw new JsonSerializationException($"Invalid date format. Expected one of: {string.Join(", ", formats)}");
    }
}
class Program
{
    static void Main()
    {
        string oldJson = "{\"date\":\"20240601\"}";
        string newJson = "{\"date\":\"2024-06-01\"}";
        var settings = new JsonSerializerSettings();
        settings.Converters.Add(new FlexibleDateTimeConverter());
        var oldObj = JsonConvert.DeserializeObject<Dictionary<string, DateTime>>(oldJson, settings);
        var newObj = JsonConvert.DeserializeObject<Dictionary<string, DateTime>>(newJson, settings);
        Console.WriteLine($"Old format date: {oldObj!["date"]:yyyy-MM-dd}");
        Console.WriteLine($"New format date: {newObj!["date"]:yyyy-MM-dd}");
    }
}
Old format date: 2024-06-01
New format date: 2024-06-01

このように、カスタムコンバーターで複数フォーマットを許容することで、バージョン間の互換性を保てます。

パフォーマンスとメモリ消費比較

シリアライズ速度の計測

JSONとDictionaryの相互変換において、シリアライズ速度は処理効率を左右する重要な指標です。

System.Text.JsonNewtonsoft.Jsonの両者で速度を計測する際は、同一のデータセットを用いてベンチマークを行うことが基本です。

例えば、10000件のDictionary<string, string>を用意し、それをJSON文字列に変換する処理を複数回繰り返して平均時間を計測します。

計測にはStopwatchクラスを使うと正確です。

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text.Json;
using Newtonsoft.Json;
class Program
{
    static void Main()
    {
        var dictionary = new Dictionary<string, string>();
        for (int i = 0; i < 10000; i++)
        {
            dictionary.Add("key" + i, "value" + i);
        }
        var sw = new Stopwatch();
        // System.Text.Json シリアライズ計測
        sw.Start();
        for (int i = 0; i < 100; i++)
        {
            string json = JsonSerializer.Serialize(dictionary);
        }
        sw.Stop();
        Console.WriteLine($"System.Text.Json Serialize: {sw.ElapsedMilliseconds} ms");
        sw.Reset();
        // Newtonsoft.Json シリアライズ計測
        sw.Start();
        for (int i = 0; i < 100; i++)
        {
            string json = JsonConvert.SerializeObject(dictionary);
        }
        sw.Stop();
        Console.WriteLine($"Newtonsoft.Json Serialize: {sw.ElapsedMilliseconds} ms");
    }
}

このように計測すると、一般的にSystem.Text.Jsonの方が高速である傾向が見られます。

ただし、データの複雑さやオプション設定によって結果は変動します。

デシリアライズ速度の計測

デシリアライズ速度も同様に重要です。

JSON文字列からDictionaryに変換する処理を繰り返し計測し、平均時間を求めます。

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text.Json;
using Newtonsoft.Json;
class Program
{
    static void Main()
    {
        var dictionary = new Dictionary<string, string>();
        for (int i = 0; i < 10000; i++)
        {
            dictionary.Add("key" + i, "value" + i);
        }
        string jsonStringSystemText = JsonSerializer.Serialize(dictionary);
        string jsonStringNewtonsoft = JsonConvert.SerializeObject(dictionary);
        var sw = new Stopwatch();
        // System.Text.Json デシリアライズ計測
        sw.Start();
        for (int i = 0; i < 100; i++)
        {
            var dict = JsonSerializer.Deserialize<Dictionary<string, string>>(jsonStringSystemText);
        }
        sw.Stop();
        Console.WriteLine($"System.Text.Json Deserialize: {sw.ElapsedMilliseconds} ms");
        sw.Reset();
        // Newtonsoft.Json デシリアライズ計測
        sw.Start();
        for (int i = 0; i < 100; i++)
        {
            var dict = JsonConvert.DeserializeObject<Dictionary<string, string>>(jsonStringNewtonsoft);
        }
        sw.Stop();
        Console.WriteLine($"Newtonsoft.Json Deserialize: {sw.ElapsedMilliseconds} ms");
    }
}

こちらも一般的にはSystem.Text.Jsonの方が高速ですが、複雑な型やカスタムコンバーターを使う場合は差が縮まることがあります。

アロケーション量とメモリ最適化

シリアライズ・デシリアライズ時のメモリ消費は、特に大規模データや高頻度処理で重要です。

System.Text.Jsonは内部でSpan<T>Utf8JsonWriterを活用し、メモリ割り当てを最小限に抑える設計となっています。

一方、Newtonsoft.Jsonは柔軟性が高い反面、オブジェクト生成や文字列操作で多くのメモリを消費する傾向があります。

特に大量の中間オブジェクトが生成されるケースでは、GC(ガベージコレクション)の負荷が増加しやすいです。

メモリ使用量を計測するには、dotnet-countersやVisual Studioの診断ツール、GC.GetTotalMemoryメソッドなどを利用します。

例えば、GC.GetTotalMemory(false)を使って処理前後のメモリ使用量を比較する方法があります。

メモリ最適化のポイントとしては、以下が挙げられます。

  • 可能な限りSpan<T>Memory<T>を活用する(System.Text.Jsonは対応済み)
  • 不要な文字列連結や中間オブジェクト生成を避ける
  • 大量データはストリーミング処理で分割して扱う
  • カスタムコンバーターで効率的な変換処理を実装する

スループット向上のポイント

JSONとDictionaryの変換でスループットを向上させるためには、以下のポイントを意識すると効果的です。

  • バッファサイズの調整

System.Text.JsonJsonSerializerOptionsでバッファサイズを調整し、I/O効率を改善できます。

  • ストリーミングAPIの活用

大量データの場合は、一括で読み書きするのではなく、Utf8JsonReaderUtf8JsonWriterを使ったストリーミング処理でメモリ負荷を軽減しつつ高速化を図れます。

  • 非同期処理の利用

ファイルやネットワークからの読み書き時は、非同期APIを使うことでスループットを向上させられます。

  • カスタムコンバーターの最適化

カスタムコンバーターを実装する際は、無駄なオブジェクト生成や複雑な処理を避け、シンプルかつ効率的なコードを書くことが重要です。

  • 不要なオプションの無効化

例えば、WriteIndentedを有効にすると可読性は上がりますが処理が遅くなるため、パフォーマンス重視なら無効にします。

  • キャッシュの活用

頻繁に同じデータを変換する場合は、変換結果をキャッシュして再利用することで処理負荷を減らせます。

これらのポイントを踏まえ、用途や環境に応じて適切な最適化を行うことで、JSONとDictionaryの変換処理のパフォーマンスとメモリ効率を最大化できます。

エラーハンドリング

代表的な例外パターン

JSONとDictionaryの相互変換を行う際に発生しやすい代表的な例外には以下のようなものがあります。

  • JsonException

JSONの構文エラーや不正な形式が原因で発生します。

例えば、JSON文字列が途中で切れている、カンマや括弧の閉じ忘れ、無効な文字が含まれている場合にスローされます。

  • InvalidOperationException

シリアライズやデシリアライズ時に、型の不整合や不正な操作が行われた場合に発生します。

例えば、System.Text.Jsonで非文字列キーのDictionaryを変換しようとした場合などです。

  • ArgumentNullException

入力のJSON文字列やDictionaryがnullの場合に発生します。

引数チェックを怠ると起こりやすい例外です。

  • FormatException

日付や数値などの特定の型変換に失敗した場合に発生します。

カスタムコンバーターで不正なフォーマットの文字列を処理しようとした際に起こることがあります。

  • JsonSerializationException(Newtonsoft.Json特有)

デシリアライズ時に型の不一致や変換エラーが発生した場合にスローされます。

例えば、期待した型と異なるJSON構造が渡された場合などです。

これらの例外は、適切なエラーハンドリングを行うことで、アプリケーションの異常終了を防ぎ、ユーザーにわかりやすいエラーメッセージを提供できます。

トラブルシューティング手順

不正JSONへの対処

不正なJSON文字列が原因で変換に失敗する場合、まずはJSONの構文を検証することが重要です。

オンラインのJSONバリデーターやツールを使って、入力データが正しい形式か確認しましょう。

プログラム内では、デシリアライズ処理をtry-catchで囲み、JsonExceptionJsonSerializationExceptionをキャッチして適切に処理します。

using System;
using System.Collections.Generic;
using System.Text.Json;
class Program
{
    static void Main()
    {
        string invalidJson = "{\"name\":\"Alice\", \"age\":30"; // 閉じ括弧がない不正JSON
        try
        {
            var dict = JsonSerializer.Deserialize<Dictionary<string, object>>(invalidJson);
        }
        catch (JsonException ex)
        {
            Console.WriteLine($"JSONの解析に失敗しました: {ex.Message}");
        }
    }
}
JSONの解析に失敗しました: '0' is an invalid end of a number. Expected a delimiter. Path: $.age | LineNumber: 0 | BytePositionInLine: 25.

また、入力元でJSONの生成を見直すことも重要です。

APIレスポンスやファイルの読み込み時にデータが破損していないか確認してください。

型不一致エラーの回避策

JSONの値が期待する型と異なる場合、デシリアライズ時にエラーが発生しやすいです。

これを回避するためには以下の方法があります。

  • 柔軟な型指定

Dictionary<string, object>Dictionary<string, JsonElement>のように、値の型を広く受け入れられる型にすることで、型不一致を回避しやすくなります。

  • カスタムコンバーターの利用

特定の型変換が必要な場合は、カスタムコンバーターを実装して、変換処理を制御します。

例えば、数値が文字列として渡されるケースに対応するなどです。

  • 事前バリデーション

JSONをデシリアライズする前に、JSONの構造や値の型をチェックする仕組みを設けると安全です。

JsonDocumentJObjectを使って動的に検査できます。

  • 例外処理の強化

デシリアライズ時に例外が発生しても処理が継続できるように、try-catchで囲み、問題のある部分だけをスキップするロジックを組み込むことも有効です。

以下は、JsonElementを使って動的に型を判別しながら安全に処理する例です。

using System;
using System.Collections.Generic;
using System.Text.Json;
class Program
{
    static void Main()
    {
        string jsonString = "{\"name\":\"Alice\",\"age\":\"30\"}"; // ageが文字列
        var dict = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(jsonString);
        foreach (var kvp in dict)
        {
            Console.Write($"Key: {kvp.Key}, Value: ");
            if (kvp.Value.ValueKind == JsonValueKind.String)
            {
                Console.WriteLine(kvp.Value.GetString());
            }
            else if (kvp.Value.ValueKind == JsonValueKind.Number)
            {
                Console.WriteLine(kvp.Value.GetInt32());
            }
            else
            {
                Console.WriteLine("Unknown type");
            }
        }
    }
}
Key: name, Value: Alice
Key: age, Value: 30

ログ出力とデバッグ支援

エラー発生時の原因特定には、詳細なログ出力が欠かせません。

JSON変換処理の前後でログを記録し、入力データや例外情報を保存することで、問題の再現や解析が容易になります。

  • 例外メッセージのログ

例外のメッセージやスタックトレースをログに残すことで、どの部分で失敗したかを把握できます。

  • 入力JSONのログ

不正なJSONが原因の場合、問題のJSON文字列をログに記録しておくと、外部ツールでの検証や修正がしやすくなります。

  • デバッグ用の詳細出力

開発環境では、デバッグレベルのログを有効にして、変換処理の各ステップを追跡できるようにします。

  • 例外の再スローや通知

必要に応じて例外を再スローしたり、監視システムに通知を送る仕組みを組み込むと、運用時の早期発見に役立ちます。

以下は、例外発生時にログを出力する簡単な例です。

using System;
using System.Collections.Generic;
using System.Text.Json;
class Program
{
    static void Main()
    {
        string invalidJson = "{\"name\":\"Alice\", \"age\":30"; // 不正JSON
        try
        {
            var dict = JsonSerializer.Deserialize<Dictionary<string, object>>(invalidJson);
        }
        catch (JsonException ex)
        {
            Console.Error.WriteLine($"[Error] JSON変換失敗: {ex.Message}");
            // ここでログファイルに書き込む処理や監視通知を追加可能
        }
    }
}

このように、適切なログ出力と例外管理を組み合わせることで、JSONとDictionaryの変換処理におけるトラブルを迅速に解決しやすくなります。

セキュリティ考慮

外部入力バリデーション

JSONデータを外部から受け取る場合、入力内容の検証はセキュリティ上非常に重要です。

悪意のあるデータや不正な形式のJSONがシステムに流入すると、アプリケーションの動作異常や情報漏洩、さらにはサービス停止につながる恐れがあります。

まず、JSON文字列の構文チェックを行い、正しい形式であることを確認します。

System.Text.JsonNewtonsoft.Jsonのデシリアライズ時に例外が発生した場合は、即座に処理を中断し、不正なデータとして扱うべきです。

さらに、JSONの内容に対してもバリデーションを実施します。

例えば、期待するキーの有無、値の型や範囲、文字列の長さ制限などをチェックし、不正な値が含まれていないかを検証します。

これにより、SQLインジェクションやクロスサイトスクリプティング(XSS)などの攻撃を防ぐことができます。

バリデーションは、単純な型チェックだけでなく、ビジネスロジックに即したルールも含めることが望ましいです。

例えば、ユーザーIDが数値であること、メールアドレスの形式が正しいこと、日付が過去や未来の不正な値でないことなどです。

置換攻撃・パススルー攻撃対策

JSONの処理においては、置換攻撃(JSON Injection)やパススルー攻撃に注意が必要です。

これらは、悪意のあるJSONデータを介してシステムの挙動を不正に操作しようとする攻撃手法です。

置換攻撃は、JSONのキーや値に特殊文字や制御文字を埋め込み、システムのパース処理や後続の処理に影響を与えるものです。

例えば、JSON文字列内にスクリプトコードやSQL文を挿入し、脆弱なシステムで実行されるリスクがあります。

対策としては、JSONのパース前後で入力データのエスケープやサニタイズを徹底することが重要です。

System.Text.JsonNewtonsoft.Jsonは標準でエスケープ処理を行いますが、独自の文字列操作やログ出力時には注意が必要です。

また、パススルー攻撃は、JSONデータをそのまま他のシステムやデータベースに渡す際に、悪意のある内容がそのまま通過してしまう問題です。

これを防ぐためには、受け取ったJSONを信頼せず、必ず検証・変換を行い、不要なデータや危険な文字列を除去することが求められます。

さらに、JSONのキーや値に予期しない型や構造が含まれていないかをチェックし、想定外のデータが処理されないように制御することも効果的です。

JSONサイズ制限とDoS防止

大量のJSONデータや極端に大きなJSON文字列を処理すると、メモリ不足やCPU負荷の増大を招き、サービス拒否(DoS)攻撃の原因となることがあります。

これを防ぐために、JSONのサイズ制限を設けることが重要です。

具体的には、受信するJSON文字列の最大バイト数や文字数を制限し、制限を超えた場合は処理を中断してエラーを返します。

これにより、過剰なリソース消費を防止できます。

また、ネストの深さにも制限を設けることが推奨されます。

深いネストはパース処理の負荷を増大させ、スタックオーバーフローや処理遅延の原因となるためです。

System.Text.JsonJsonReaderOptions.MaxDepthNewtonsoft.JsonMaxDepthプロパティで設定可能です。

さらに、ストリーミング処理を活用し、大きなJSONを分割して逐次処理することで、メモリ使用量を抑制できます。

これらの対策を組み合わせることで、JSON処理におけるDoS攻撃リスクを低減し、安全なシステム運用が可能となります。

Dictionaryの特殊ケース

キーが複合型の場合

通常、Dictionaryのキーは文字列や整数などの単純な型が使われますが、複合型(例えばクラスや構造体)をキーにしたいケースもあります。

しかし、JSONシリアライズでは複合型のキーは直接的にサポートされていません。

これはJSONのオブジェクトのキーが文字列である必要があるためです。

複合型のキーを持つDictionaryをJSONに変換するには、キーを文字列に変換するカスタムコンバーターを実装する方法が一般的です。

キーの複合型を一意に表現できる文字列形式(例えばJSON文字列化や特定のフォーマット)に変換し、シリアライズ時に文字列として出力、デシリアライズ時に元の複合型に復元します。

以下は、複合型のキーを持つDictionarySystem.Text.Jsonで扱う例です。

using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
struct ComplexKey
{
    public int Id { get; set; }
    public string Name { get; set; }
    public override string ToString() => $"{Id}:{Name}";
}
class ComplexKeyDictionaryConverter<TValue> : JsonConverter<Dictionary<ComplexKey, TValue>>
{
    public override Dictionary<ComplexKey, TValue> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        var dict = new Dictionary<ComplexKey, TValue>();
        if (reader.TokenType != JsonTokenType.StartObject)
            throw new JsonException();
        while (reader.Read())
        {
            if (reader.TokenType == JsonTokenType.EndObject)
                return dict;
            string keyString = reader.GetString();
            var parts = keyString.Split(':');
            if (parts.Length != 2 || !int.TryParse(parts[0], out int id))
                throw new JsonException("Invalid key format");
            var key = new ComplexKey { Id = id, Name = parts[1] };
            reader.Read();
            TValue value = JsonSerializer.Deserialize<TValue>(ref reader, options);
            dict.Add(key, value);
        }
        throw new JsonException();
    }
    public override void Write(Utf8JsonWriter writer, Dictionary<ComplexKey, TValue> value, JsonSerializerOptions options)
    {
        writer.WriteStartObject();
        foreach (var kvp in value)
        {
            writer.WritePropertyName(kvp.Key.ToString());
            JsonSerializer.Serialize(writer, kvp.Value, options);
        }
        writer.WriteEndObject();
    }
}
class Program
{
    static void Main()
    {
        var dict = new Dictionary<ComplexKey, string>
        {
            { new ComplexKey { Id = 1, Name = "A" }, "Value1" },
            { new ComplexKey { Id = 2, Name = "B" }, "Value2" }
        };
        var options = new JsonSerializerOptions();
        options.Converters.Add(new ComplexKeyDictionaryConverter<string>());
        options.WriteIndented = true;
        string json = JsonSerializer.Serialize(dict, options);
        Console.WriteLine(json);
        var deserialized = JsonSerializer.Deserialize<Dictionary<ComplexKey, string>>(json, options);
        foreach (var kvp in deserialized)
        {
            Console.WriteLine($"Key: Id={kvp.Key.Id}, Name={kvp.Key.Name}, Value: {kvp.Value}");
        }
    }
}
{
  "1:A": "Value1",
  "2:B": "Value2"
}
Key: Id=1, Name=A, Value: Value1
Key: Id=2, Name=B, Value: Value2

このように、複合型のキーを文字列に変換して扱うことで、JSONとの相互変換が可能になります。

値がNullable型の場合

Dictionaryの値がNullable<T>型(例えばint?DateTime?)の場合、null値の扱いに注意が必要です。

JSONではnullは明示的に表現できるため、null値はJSONのnullとしてシリアライズされます。

ただし、シリアライズ時にnull値を省略したい場合は、System.Text.JsonJsonSerializerOptions.IgnoreNullValues(.NET 5.0以降はDefaultIgnoreConditionに変更)や、Newtonsoft.JsonNullValueHandling.Ignoreを設定します。

以下は、Nullable<int>を値に持つDictionaryの例です。

using System;
using System.Collections.Generic;
using System.Text.Json;
class Program
{
    static void Main()
    {
        var dict = new Dictionary<string, int?>
        {
            { "a", 10 },
            { "b", null },
            { "c", 30 }
        };
        var options = new JsonSerializerOptions
        {
            IgnoreNullValues = true,
            WriteIndented = true
        };
        string json = JsonSerializer.Serialize(dict, options);
        Console.WriteLine(json);
    }
}
{
  "a": 10,
  "c": 30
}

"b"のキーは値がnullなのでJSONに含まれていません。

逆にIgnoreNullValuesfalseにすると、"b": nullとして出力されます。

デシリアライズ時もnull値は正しくNullable<T>にマッピングされます。

循環参照を含むオブジェクト

Dictionaryの値に循環参照を含むオブジェクトがある場合、シリアライズ時に無限ループや例外が発生することがあります。

例えば、オブジェクトAがオブジェクトBを参照し、Bが再びAを参照しているケースです。

System.Text.Jsonはデフォルトで循環参照を検出すると例外をスローします。

これを回避するには、JsonSerializerOptions.ReferenceHandlerReferenceHandler.Preserveを設定し、循環参照をサポートする方法があります。

using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
class Node
{
    public string Name { get; set; }
    public Node? Next { get; set; }
}
class Program
{
    static void Main()
    {
        var node1 = new Node { Name = "Node1" };
        var node2 = new Node { Name = "Node2" };
        node1.Next = node2;
        node2.Next = node1; // 循環参照
        var dict = new Dictionary<string, Node>
        {
            { "first", node1 }
        };
        var options = new JsonSerializerOptions
        {
            WriteIndented = true,
            ReferenceHandler = ReferenceHandler.Preserve
        };
        string json = JsonSerializer.Serialize(dict, options);
        Console.WriteLine(json);
    }
}
{
  "$id": "1",
  "first": {
    "$id": "2",
    "Name": "Node1",
    "Next": {
      "$id": "3",
      "Name": "Node2",
      "Next": {
        "$ref": "2"
      }
    }
  }
}

ReferenceHandler.Preserveを使うと、循環参照を特殊な$id$refで表現し、無限ループを防ぎます。

Newtonsoft.JsonでもPreserveReferencesHandlingオプションで同様の対応が可能です。

既定値と省略値の扱い

Dictionaryの値に既定値(デフォルト値)が含まれる場合、シリアライズ時にそれらを省略するかどうかを制御できます。

例えば、intの既定値は0boolfalseです。

System.Text.Jsonでは、.NET 5.0以降でJsonSerializerOptions.DefaultIgnoreConditionJsonIgnoreCondition.WhenWritingDefaultを設定すると、既定値のプロパティや要素をJSONに含めないようにできます。

using System;
using System.Collections.Generic;
using System.Text.Json;
class Program
{
    static void Main()
    {
        var dict = new Dictionary<string, int>
        {
            { "a", 0 },
            { "b", 5 }
        };
        var options = new JsonSerializerOptions
        {
            DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault,
            WriteIndented = true
        };
        string json = JsonSerializer.Serialize(dict, options);
        Console.WriteLine(json);
    }
}
{
  "b": 5
}

"a"の値は0(既定値)なので省略されています。

Newtonsoft.Jsonでは、DefaultValueHandling.Ignoreを設定することで同様の動作が可能です。

既定値の省略はJSONのサイズ削減に有効ですが、受け取る側で既定値を補完するロジックが必要になるため、仕様に応じて使い分けることが重要です。

最適化パターン

オンメモリキャッシュとの併用

JSONとDictionaryの変換処理は、頻繁に同じデータを扱う場合、毎回シリアライズやデシリアライズを行うとパフォーマンスに影響が出ることがあります。

そこで、オンメモリキャッシュを活用して変換結果を再利用する方法が効果的です。

例えば、APIレスポンスや設定データなど、変更頻度が低いデータは一度変換した結果をメモリ上にキャッシュしておき、次回以降はキャッシュから取得することで処理時間を大幅に短縮できます。

以下は、簡単なキャッシュ機構を使った例です。

using System;
using System.Collections.Generic;
using System.Text.Json;
class JsonCache
{
    private readonly Dictionary<string, Dictionary<string, string>> _cache = new();
    public Dictionary<string, string> GetOrAdd(string json)
    {
        if (_cache.TryGetValue(json, out var dict))
        {
            Console.WriteLine("キャッシュから取得");
            return dict;
        }
        dict = JsonSerializer.Deserialize<Dictionary<string, string>>(json);
        _cache[json] = dict;
        Console.WriteLine("新規デシリアライズ");
        return dict;
    }
}
class Program
{
    static void Main()
    {
        var cache = new JsonCache();
        string json = "{\"key1\":\"value1\",\"key2\":\"value2\"}";
        var dict1 = cache.GetOrAdd(json); // 新規デシリアライズ
        var dict2 = cache.GetOrAdd(json); // キャッシュから取得
    }
}
新規デシリアライズ
キャッシュから取得

このように、同じJSON文字列に対してはキャッシュを利用することで、無駄な変換処理を省けます。

ただし、キャッシュのサイズ管理や更新タイミングには注意が必要です。

ストリーミングシリアライズ

大量のデータを一括でシリアライズ・デシリアライズすると、メモリ消費が増大し、パフォーマンス低下やOutOfMemory例外の原因になります。

これを防ぐために、ストリーミングAPIを活用して逐次的に処理する方法があります。

System.Text.Jsonでは、Utf8JsonWriterを使ってストリームに直接JSONを書き込むことが可能です。

これにより、メモリ上に全体のJSON文字列を保持せずに済み、大規模データの処理に適しています。

以下は、Dictionary<string, string>をストリーミングでシリアライズする例です。

using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
class Program
{
    static void Main()
    {
        var dict = new Dictionary<string, string>
        {
            { "name", "Alice" },
            { "city", "Tokyo" },
            { "language", "C#" }
        };
        using var stream = new MemoryStream();
        using (var writer = new Utf8JsonWriter(stream, new JsonWriterOptions { Indented = true }))
        {
            writer.WriteStartObject();
            foreach (var kvp in dict)
            {
                writer.WriteString(kvp.Key, kvp.Value);
            }
            writer.WriteEndObject();
        }
        string json = System.Text.Encoding.UTF8.GetString(stream.ToArray());
        Console.WriteLine(json);
    }
}
{
  "name": "Alice",
  "city": "Tokyo",
  "language": "C#"
}

ストリーミングシリアライズは、ファイルやネットワークへの書き込み時に特に有効で、メモリ使用量を抑えつつ高速に処理できます。

Span<char>とUtf8JsonWriter活用

System.Text.JsonSpan<char>Utf8JsonWriterを活用することで、メモリ割り当てを最小限に抑え、高速なJSON処理を実現しています。

Span<char>はスタック上の連続したメモリ領域を表し、ヒープ割り当てを減らせるため、GC負荷の軽減に寄与します。

例えば、JSON文字列の一部をSpan<char>で操作したり、Utf8JsonWriterでバイト単位の書き込みを行うことで、余計な文字列生成やコピーを避けられます。

以下は、Utf8JsonWriterを使って部分的にJSONを生成し、Span<byte>を活用する例です。

using System;
using System.Buffers;
using System.Text;
using System.Text.Json;
class Program
{
    static void Main()
    {
        var buffer = new ArrayBufferWriter<byte>();
        using (var writer = new Utf8JsonWriter(buffer, new JsonWriterOptions { Indented = true }))
        {
            writer.WriteStartObject();
            writer.WriteString("message", "Hello, Span!");
            writer.WriteNumber("year", 2024);
            writer.WriteEndObject();
        }
        ReadOnlySpan<byte> jsonSpan = buffer.WrittenSpan;
        string json = Encoding.UTF8.GetString(jsonSpan);
        Console.WriteLine(json);
    }
}
{
  "message": "Hello, Span!",
  "year": 2024
}

この方法は、文字列の中間生成を減らし、パフォーマンスとメモリ効率を向上させます。

特に高頻度のJSON処理や大規模データのシリアライズに適しています。

これらの最適化パターンを組み合わせることで、JSONとDictionaryの変換処理を効率的かつスケーラブルに実装できます。

他言語・環境との相互運用

JavaScriptとのデータ往復

C#のDictionaryとJavaScriptのオブジェクトは、どちらもキーと値のペアを扱うデータ構造であり、JSONを介して簡単にデータのやり取りが可能です。

JavaScript側ではJSON文字列をJSON.parseでオブジェクトに変換し、C#側ではDictionary<string, object>Dictionary<string, string>にデシリアライズします。

例えば、C#で生成したJSONをJavaScriptで受け取り、操作した後に再びJSONに変換してC#に送信する流れは以下のようになります。

C#側(JSON生成)

using System;
using System.Collections.Generic;
using System.Text.Json;
class Program
{
    static void Main()
    {
        var dict = new Dictionary<string, string>
        {
            { "username", "alice" },
            { "role", "admin" }
        };
        string json = JsonSerializer.Serialize(dict);
        Console.WriteLine(json);
    }
}
{"username":"alice","role":"admin"}

JavaScript側(受信と送信)

// 受信したJSON文字列
const jsonString = '{"username":"alice","role":"admin"}';
// JSON文字列をオブジェクトに変換
const obj = JSON.parse(jsonString);
console.log(obj.username); // "alice"
// オブジェクトを操作
obj.role = "user";
// オブジェクトをJSON文字列に変換して送信
const updatedJson = JSON.stringify(obj);
console.log(updatedJson);
alice
{"username":"alice","role":"user"}

このように、JSONを介してC#のDictionaryとJavaScriptのオブジェクト間でデータを往復させることができます。

注意点としては、JavaScriptのオブジェクトのキーは文字列であるため、C#のDictionaryのキーも文字列であることが望ましいです。

また、値の型に互換性があるかを確認し、必要に応じて型変換やバリデーションを行うことが重要です。

REST APIでの送受信

REST APIを介してJSONデータを送受信する場合、C#のDictionaryとJSONの相互変換は頻繁に利用されます。

APIのリクエストボディやレスポンスボディにJSONを使い、C#側でDictionaryに変換して処理し、結果を再びJSONにシリアライズして返す流れが一般的です。

例えば、ASP.NET CoreのWeb APIでDictionaryを受け取り、処理して返す簡単な例を示します。

using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
[ApiController]
[Route("api/[controller]")]
public class SampleController : ControllerBase
{
    [HttpPost("process")]
    public IActionResult ProcessData([FromBody] Dictionary<string, string> data)
    {
        // 受け取ったDictionaryを操作
        data["processed"] = "true";
        // 変更したDictionaryをJSONで返す
        return Ok(data);
    }
}

クライアントはJSON形式でデータを送信し、レスポンスもJSONで受け取ります。

C#のモデルバインディングが自動的にJSONをDictionaryに変換し、返却時にはJSONにシリアライズします。

REST APIでの注意点としては、以下が挙げられます。

  • Content-Typeヘッダーの設定

リクエストとレスポンスのContent-Typeapplication/jsonに設定する必要があります。

  • バリデーション

受信したJSONの構造や値を検証し、不正なデータを防ぐことが重要です。

  • エラーハンドリング

変換エラーやビジネスロジックの例外を適切に処理し、クライアントにわかりやすいエラーメッセージを返します。

  • セキュリティ

JSONインジェクションやDoS攻撃を防ぐための対策を講じます。

非UTF8エンコーディングの取り扱い

JSONは基本的にUTF-8エンコーディングで扱われることが標準ですが、環境や要件によってはShift_JISやISO-8859-1などの非UTF8エンコーディングのJSONを扱う必要がある場合があります。

C#で非UTF8のJSONを処理する場合、文字列として直接扱うのではなく、バイト配列やストリームを適切なエンコーディングで読み書きすることが重要です。

非UTF8のJSONを読み込む例(Shift_JIS)

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.Json;
class Program
{
    static void Main()
    {
        // Shift_JISでエンコードされたJSONファイルのパス
        string filePath = "data_sjis.json";
        // Shift_JISエンコーディングでファイルを読み込む
        using var stream = new FileStream(filePath, FileMode.Open);
        using var reader = new StreamReader(stream, Encoding.GetEncoding("shift_jis"));
        string jsonString = reader.ReadToEnd();
        // JSON文字列をDictionaryにデシリアライズ
        var dictionary = JsonSerializer.Deserialize<Dictionary<string, string>>(jsonString);
        foreach (var kvp in dictionary)
        {
            Console.WriteLine($"{kvp.Key}: {kvp.Value}");
        }
    }
}

非UTF8のJSONを書き込む例(Shift_JIS)

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.Json;
class Program
{
    static void Main()
    {
        var dict = new Dictionary<string, string>
        {
            { "名前", "太郎" },
            { "都市", "東京" }
        };
        string jsonString = JsonSerializer.Serialize(dict);
        // Shift_JISエンコーディングでファイルに書き込む
        using var stream = new FileStream("output_sjis.json", FileMode.Create);
        using var writer = new StreamWriter(stream, Encoding.GetEncoding("shift_jis"));
        writer.Write(jsonString);
    }
}

非UTF8エンコーディングを扱う際は、JsonSerializer自体は文字列のシリアライズ・デシリアライズを行うため、エンコーディングの変換はストリームやファイルの読み書き時に行うことがポイントです。

また、API通信などで非UTF8のJSONを扱う場合は、HTTPヘッダーのContent-Typeに適切な文字セットを指定し、クライアント・サーバー双方でエンコーディングを一致させる必要があります。

まとめ

この記事では、C#でJSONとDictionaryを高速かつ柔軟に相互変換する方法を解説しました。

System.Text.JsonNewtonsoft.Jsonの基本的な使い方から、カスタムコンバーターの実装、パフォーマンス最適化、エラーハンドリング、セキュリティ対策まで幅広くカバーしています。

また、他言語や環境との連携や特殊ケースの対応方法も紹介し、実践的な知識を提供しました。

これにより、効率的で安全なJSON処理が実現できます。

関連記事

Back to top button
目次へ