【C#】System.Text.Jsonでオブジェクトのシリアライズ・デシリアライズ

.NET Core 3.0 から利用可能な新しい標準ライブラリ JSON シリアライザー System.Text.JsonJsonSerializer 使い方の紹介です。

動作環境

以下環境で標準で使用することができます。

  • 使用可能な.NETのバージョン
    • .NET Core3.0~
    • .NET Standard 2.1~
    • .NET Framework 4.8~

使い方

早速使い方の紹介です。

まず以下の名前空間を using します。

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

今回は以下のようなJSONデータを扱います。

ルート要素をオブジェクト配列として扱います。

[
  {
      "id": 0,
      "name": "Taka",
      "numbers": [ 0, 1, 2, 3 ],
      "list": [ 0, 1, 2, 3 ],
      "map": {
          "key1": "value1",
          "key2": "value2",
          "key3": "value3",
          "key4": "value4",
          "key5": "value5"
      }
  },
  {
      "id": 1,
      "name": "PG",
      "numbers": [ 10, 11, 12, 13 ],
      "list": [10, 20, 30],
      "map": {
        "aaa": "111",
        "bbb": "222",
        "ccc": "333"
      }
  }
]

次に、シリアライズ・デシリアライズするクラスを宣言します。JSON に対応するクラスを作成し、そこに JSON データを当てはめていきます。

// JsonItem.cs

public class JsonItem
{
    // ★★★ プロパティが自動的にシリアライズの対象に選ばれる
    //        オブジェクトに別名を付けたい場合以下のように JsonPropertyName 属性を付ける
    [JsonPropertyName("id")]
    public int ID { get; set; }

    // ★★★ フィールドはシリアライズの対象にできない(指定しても無視される
    //        プロパティだけシリアライズの対象できる
    [JsonPropertyName("asdf")]
    public string sample = "";

    [JsonPropertyName("name")]
    public string Name { get; set; }

    [JsonPropertyName("numbers")]
    public int[] Numbers { get; set; }

    [JsonPropertyName("list")]
    public List<float> NumberList { get; private set; } = new List<float>();

    [JsonPropertyName("map")]
    public IDictionary<string, string> Attributes { get; private set; } 
        = new Dictionary<string, string>();
    
    // ★★★ 除外するプロパティには JsonIgnore 属性を付ける
    [JsonIgnore]
    public string PrivacyInfo { get; set; }
}

// 配列として扱うためリストにオブジェクトを格納する
var items = new List<JsonItem>();

1点注意があります。System.Text.Json のシリアライズ対象はプロパティのため、フィールドはシリアライズできません(このため Unity で JsonUtility を使用してる場合の乗り換えは難しいかもしれません)

プロパティ名と JSON 内のノードの名異なる場合「JsonPropertyName (System.Text.Json.Serialization名前空間)」を上記コード例のように付与します。また、デフォルトでは自動的にプロパティが全部出力されるのでシリアライズ対象にしたくないプロパティには「JsonIgnore」を同じく付与します。経験上、プロパティ名 == ノード名だったとしてもJsonPropetyName は指定しておいたほうが無難です。

リスト も Dictionary も配列もすべてサポートしているので一般的な .NET の型はほぼすべてカバーしています。ただし入れ子になったオブジェクト階層は 64までなので深すぎるオブジェクトだけ注意しましょう。

シリアライズ:文字列 → オブジェクト

シリアライズ方法は以下の通りです。

// あらかじめ適当なデータを生成しておく
List<JsonItem> items = SampleData.GetJsonItems(DataLength);

// ★★★ 必要に応じて以下のオプションを指定する
var op = new JsonSerializerOptions
{
    // ★★★ (1) 文字列にマルチバイト文字(≒日本語)が含まれる
    //  → 指定しないと文字コードが出力される
    Encoder = JavaScriptEncoder.Create(UnicodeRanges.All),

    // ★★★ (2) 出力結果をインデントするかどうか
    // true : 空白文字列で整形される / false : されない
    WriteIndented = true,

    // ★★★ (3) null を無視するかどうか
    // true : nullならJSONに出力されない
    // false : 出力する(既定はこっち)
    IgnoreNullValues = true,

    // ★★★ (4) 読み取り専用プロパティを無視するかどうか
    // true : 読み取り専用(getのみ)のプロパティはJSONに出力しない
    // false : 出力する(既定はこっち)
    IgnoreReadOnlyProperties = true,
    
    // ★★★ (5) 出力をCamelケースにするかの指定
    // JsonPropertyName 指定があるとそっちが優先される
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};

// オブジェクトをJSON文字列にシリアライズする
string jsonStr = JsonSerializer.Serialize(item, op);

必要に応じてコード例のコメントの通りオプションを指定します。日本語を含むマルチバイト文字を扱う場合 Encoder を指定します。他のオプションは状況に応じて指定します。他のオプションについては公式のリファレンスを参照してください。

JSON化したデータは以下のようなデータとして jsonStr 変数に入っています。

// jsonStr 変数の中身
[
  {
    "time": "2020-08-21T00:14:58.0850867+09:00",
    "span": "10200",
    "id": 1905413078,
    "name": "2d9f9caa-9f05-4213-a992-3e5f6672c575",
    "numbers": [
      1512193737,
      300677771,
      1625274804,
      1946273800,
      251410219
    ],
    "list": [
      263971600,
      1.634365E+09,
      550383800,
      1.4658991E+09,
      203363660
    ],
    "map": {
      "feb9c436-6e85-453b-98ce-395f549d7b2e": "b1763a54-c41c-4b46-b424-cebf87271469",
      "c23bf690-33ae-4096-998d-0901b09e4ec9": "ef737ce1-5185-4e57-a009-edd538c7ae57",
      "44598508-d4e1-44d8-89a3-4ce3ce241215": "c33f3104-3a8e-46fc-90cb-8d9fcb0952de",
      "4b1a697c-c17e-4458-9e61-ec0f222df2cf": "4bc7eac1-3160-4496-8667-dc11e67979e6",
      "c1d3f90f-64ba-4f9e-a43b-bf97b20d144c": "47fd7ae3-a194-46c4-b299-e60bc9d83ea2"
    }
  }
]

参考までにデータ生成は以下の通り処理を行っています。

public static class SampleData
{
    private static readonly Random r = new Random();

    // 指定した件数分のJSONに変換するデータを生成する
    public static List<JsonItem> GetJsonItems(int count)
    {
        IEnumerable<JsonItem> f()
        {
            for (int i = 0; i < count; i++)
            {
                var item = new JsonItem()
                {
                    ID = r.Next(),
                    Name = Guid.NewGuid().ToString(),
                    Numbers = new int[]
                    {
                    r.Next(),
                    r.Next(),
                    r.Next(),
                    r.Next(),
                    r.Next(),
                    },
                };

                item.NumberList.Add(r.Next());
                item.NumberList.Add(r.Next());
                item.NumberList.Add(r.Next());
                item.NumberList.Add(r.Next());
                item.NumberList.Add(r.Next());

                item.Attributes[Guid.NewGuid().ToString()] = Guid.NewGuid().ToString();
                item.Attributes[Guid.NewGuid().ToString()] = Guid.NewGuid().ToString();
                item.Attributes[Guid.NewGuid().ToString()] = Guid.NewGuid().ToString();
                item.Attributes[Guid.NewGuid().ToString()] = Guid.NewGuid().ToString();
                item.Attributes[Guid.NewGuid().ToString()] = Guid.NewGuid().ToString();

                yield return item;
            }
        }

        return f().ToList();
    }
}

デシリアライズ:オブジェクト → 文字列

デシリアライズの方法は以下の通りです。

先ほどの jsonStr をデシリアライズすると元の List に戻すことができます。

// string jsonStr = JsonSerializer.Serialize(item, op);

// デシリアライズ方法のオプションの指定
op = new JsonSerializerOptions()
{
    // ★★★ (1) デシリアライズするときに大文字と小文字を区別するかしないかのフラグ
    // true : 区別しない
    // fa;se : 区別する(既定)
    PropertyNameCaseInsensitive = true,

    // ★★★ (2) JSONにコメントが含まれる場合の扱い方
    // Allow : 許可する ← なぜかエラーが出る…たぶん使えない
    // Disallow : 許可しない(検出した場合例外:これが既定値)
    // Skip : その行はスキップする
    ReadCommentHandling = JsonCommentHandling.Skip,

    // 以下の2種類の標準日準拠のコメントが対象にできる
    // {
    //    "TemperatureCelsius": 25, // Fahrenheit 77
    //    "Summary": "Hot" /* Zharko */
    // }

    // ★★★ (3) 末尾にコンマ記号がついているJSONを許可するかどうか
    // true : 許可する
    // false : 許可しない(既定)
    AllowTrailingCommas = true,

    // 本来以下のように末尾にコンマ記号がある標準非準拠のJSONを扱うかどうか
    // {
    //    "Item1": "a",
    //    "Item2": "b", ★★★末尾にコンマ
    // }
};

// オブジェクトにデシリアライズする
List<JsonItem> list = JsonSerializer.Deserialize<List<JsonItem>>(jsonStr, op);

これもオプションを指定することができます。

標準に準拠しないJSONをどう扱うかなどのオプションが用意されています。

ストリーム(ファイル)に読み書きする

書き込み方は以下の通りです。

// ファイルに書き出す

var options = new JsonSerializerOptions
{
    // UTF-8が確実かつエスケープ規則が既知ならこれを指定する
    Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
};

// 出力先の指定
using var stream = new FileStream(@"c:\tmp\sample.json", FileMode.Create, FileAccess.Write);

// ストリームに対してシリアライズした結果を書き出す
await JsonSerializer.SerializeAsync(stream  // 出力するストリーム, 
                                    items,  // 出力するオブジェクト
                                    op); // オプション(任意)

// WebRequest のレスポンスに書きこむこともできる
// Stream dataStream = request.GetRequestStream();
// await JsonSerializer.SerializeAsync(dataStream, sourceItem, op);

読み込み方法は以下の通り

// ファイルから読み込む

var options = new JsonSerializerOptions
{
    Encoder = JavaScriptEncoder.Create(UnicodeRanges.All),
};

// 読み取り先の指定
using var stream = new FileStream(@"c:\tmp\sample.json", FileMode.Open, FileAccess.Read);
// ストリームから読み取った内容をオブジェクトに復元する
var itemList =  
    await JsonSerializer.DeserializeAsync<List<JsonItem>>(stream, options);

その他の規則

DateTime 、TimeSpan の扱い

以下、標準の状態で DateTime と TimeSpan の変換を見てみます。

// ★ DateTime 型をシリアライズしたときのJSON
[JsonPropertyName("time")]
public DateTime Time { get; set; } = DateTime.Now;
// > "Time": "2020-08-20T11:41:03.7557392+09:00",

// ★ TimeSpan 型をシリアライズしたときのJSON
[JsonPropertyName("span")]
public TimeSpan Span { get; set; } = TimeSpan.FromSeconds(10.2);
// > "Span": {},
//  → 変換できない!

TimeSpan 型は変換できないみたいです。

派生クラスのシリアライズ

派生クラスの場合以下のようにひと手間必要です。ちょっと違和感がありますがお約束だと思ってあきらめましょう。

// 基底クラスを出力する場合工夫が必要
public class A { }
public class B : A { }

B b = new B();
JsonSerializer.Serialize(b); // これだとBの階層しか出力されない
JsonSerializer.Serialize<object>(b); // ジェネリックに<object>を指定すると基底クラスも出力される

カスタムコンバーターを使う

上記で出力されない型を独自の規則で入出力するには自作の Converter を実装する必要があります

先ほどシリアライズできなかった TimeSpan 型をシリアライズ・デシリアライズできるようにするためには以下のようにコンバーターを実装します。

// TimeSpan ⇔ JSONに変換するコンバーター
public class TimeSpanConverter : JsonConverter<TimeSpan>
{
    public override TimeSpan 
        Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        // 文字列の数値をミリ秒で変換
        return TimeSpan.FromMilliseconds(double.Parse(reader.GetString()));

        // 数値の場合以下のように書く
        // return TimeSpan.FromMilliseconds(reader.GetDouble());
    }

    public override void
        Write(Utf8JsonWriter writer, TimeSpan value, JsonSerializerOptions options)
    {
        writer.WriteStringValue(value.TotalMilliseconds.ToString()); // ミリ秒で書き出し
        // writer.WriteNumberValue(value.TotalMilliseconds); // 数値で書き出すときはこう
    }
}

で、プロパティに以下のように記述します。

[JsonConverter(typeof(TimeSpanConverter))]
public TimeSpan Span { get; set; } 

他にもいろいろ変換方法を指定する方法はありますがこれだけ覚えておけば大抵大丈夫だと思います(他にもやり方があるということだけ覚えておいてこれで対応できそうもなければここまで記事を見ていればリファレンスを参照すれば十分対応可能かと思います)

DateTime 型は表現方法が色々あるので標準でなくカスタムコンバーターを記述することもありそうです。

既存のシリアライザーとの動作速度の比較

最後に手元で適当に計測したら速度は以下の通りです。

System.Text.Json > Json.NET >> DataContractSerializer

実行速度のおおまかな比率はシリアライズもデシリアライズも大凡以下の通りです。

DataContractSerializer を 「1」 とした場合、JSON.NET「1.2~1.5 倍速い」、System.Text.Json「2.1~2.9 倍速い」

新規開発の場合とりあえず System.Text.Json をシリアライザとして選択するのがよさそうです。MSDN リファレンスに Newtonsoft.Jsonから移行する方法という移行のサポートページが公開されているため一読するとよさそうです。

関連記事

もう使うことは無いかもしれませんが、これ以前のオブジェクトのシリアライズの方法については以下の通りです。

takap-tech.com