TERRYのブログ

最近は競技プログラミングにお熱。

Sponsored Link

【C#】System.Text.Jsonを使ってみる

はじめに

皆さん、JSON使ってますか?C#でJSONを使うならJson.NETが定番中の定番ですが、.NET Core 3.0以降からSystem.Text.Jsonが標準で入るようになりました。一通り触ってみたので備忘録としてまとめておきます。

何が嬉しいの?

メリット

  • サードパーティーライブラリに依存せずに済む
  • 配布ファイルのサイズ削減
  • UTF-16を介さずUTF-8文字列を直接読み書きできるので高速(らしい)1
  • あとなんだろ……

デメリット

  • Json.NETからの移行には一部コードの修正が必要
  • Json.NETで出来ていたことが一部出来ない
  • 厳密な分あまり融通が利かない(パフォーマンス重視?)
  • (現時点では)マイナーなので参考にできるサンプルが少ない

くらいでしょうか?今のところパフォーマンスが重要になる場面以外ではあんまり魅力的じゃない気もしますが、これからきっと流行ると願って……願って……。

導入

.NET Core 3.0以降

何もせずとも最初から入ってます。

NET Standard 2.0 以降、.NET Framework 4.6.1以降2、.NET Core 2.x

nugetから落としましょう。

シリアライズ

まずは普通に

こんなクラスがあったとして、

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public override string ToString() => $"Name:{Name}, Age:{Age}";
}

Json.NETみたいにこうやると、

using System;
using System.Text.Json;

var customers = new Person[]
{
    new Person() { Name = "山田", Age = 29 },
    new Person() { Name = "鈴木", Age = 22 }
};

string json = JsonSerializer.Serialize(customers);
Console.WriteLine(json);

出力は、

[{"Name":"\u5C71\u7530","Age":29},{"Name":"\u9234\u6728","Age":22}]

あれ?となります。これはSystem.Text.Jsonが既定で基本ラテン文字以外をエスケープする仕様になっているからです。3人間が読む必要がなければこれでも良いですが、エスケープしないようにするには以下のように書いてあげます。ついでにWriteIndented = trueとしてインデントを付けてあげました。

using System;
using System.Text.Encodings.Web;
using System.Text.Json;

// (中略)

var options = new JsonSerializerOptions
{
    // JavaScriptEncoder.Createでエンコードしない文字を指定するのも可
    Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, 
    // 読みやすいようインデントを付ける
    WriteIndented = true 
};

string json = JsonSerializer.Serialize(customers, options);
Console.WriteLine(json);

出力

[
  {
    "Name": "山田",
    "Age": 29
  },
  {
    "Name": "鈴木",
    "Age": 22
  }
]

UTF-8ストリームに直接書き込む

JSONを使う場合って、ファイル操作やネットワーク処理など何らかのStreamを読み書きすることが多いと思います。この場合は直接Streamを渡してあげましょう。4

using System;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.IO;

// (中略)

var options = new JsonSerializerOptions
{
    Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, 
    WriteIndented = true 
};

// FileStreamじゃなくてもStreamならOK
using var stream = new FileStream(fileName, FileMode.Create, FileAccess.Write);
// optionsは省略可。必要に応じてCancellationTokenも渡す。
await JsonSerializer.SerializeAsync(stream, customers, options);

Utf8JsonWriterを引数に取るJsonSerializer.Serializeのオーバーロードもあるみたいですが割愛。

プロパティ名をcamelCaseにする

camelCaseにしたいだけなら、オプションでJsonNamingPolicy.CamelCaseを指定してあげれば良いです。

// (前略)
var options = new JsonSerializerOptions
{
    Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
    WriteIndented = true,
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
// (後略)

出力

[
  {
    "name": "山田",
    "age": 29
  },
  {
    "name": "鈴木",
    "age": 22
  }
]

任意のプロパティ名にする

任意のプロパティ名にする場合は、対象のクラスにJsonPropertyName属性を付けてあげます。 ちなみにJsonNamingPolicy.CamelCaseと同時に指定した場合、JsonPropertyName属性の方が優先されます。

using System.Text.Json.Serialization;

public class Person
{
    [JsonPropertyName("name")]
    public string Name { get; set; }
    [JsonPropertyName("age")]
    public int Age { get; set; }
    public override string ToString() => $"Name:{Name}, Age:{Age}";
}

デシリアライズ

stringからデシリアライズ

雰囲気はJson.NETと同じです。オプションも適宜指定してあげてください。

using System;
using System.Text.Encodings.Web;
using System.Text.Json;

string json = "[{\"name\":\"山田\",\"age\":29},{\"name\":\"鈴木\",\"age\":22}]";

var options = new JsonSerializerOptions
{
    Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};

// optionsは省略可
Person[] customers = JsonSerializer.Deserialize<Person[]>(json, options);

foreach (var customer in customers)
{
    Console.WriteLine(customer);
}

出力

Name:山田, Age:29
Name:鈴木, Age:22

Streamからデシリアライズ

特に書くことないです。

using System;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.IO;

var options = new JsonSerializerOptions
{
    Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};

using var stream = new FileStream(fileName, FileMode.Open, FileAccess.Read);
// optionsは省略可
Person[] customers = await JsonSerializer.DeserializeAsync<Person[]>(stream, options);

大文字小文字を区別しない

オプションにPropertyNameCaseInsensitive = trueを指定します。

var options = new JsonSerializerOptions
{
    Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
    PropertyNameCaseInsensitive = true
};
Person[] customers = JsonSerializer.Deserialize<Person[]>(json, options);

コメントや末尾のコンマを許可

RFC 8259ではコメントや末尾のコンマは許可されていないため、そういったJSONをデシリアライズしようとすると例外が発生します。これらを許可する場合は以下のようにオプションを指定します。

var options = new JsonSerializerOptions
{
    Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
    // コメントを許可
    ReadCommentHandling = JsonCommentHandling.Skip,
    // 末尾のコンマを許可
    AllowTrailingCommas = true
};
Person[] customers = JsonSerializer.Deserialize<Person[]>(json, options);

その他もろもろ

ドキュメント見て

まとめ

というわけでSystem.Text.Jsonの使い方覚え書きでした。あまり凝ったことしなければJson.NETみたいな感覚でまあ普通に使えるかなという感触です。パフォーマンスも気になるところなので、気が向いたら測定してみようかなあと思います。

参考

脚注


  1. 中の人曰くシリアライズで1.5倍、デシリアライズで2倍高速……らしい。ほんまか?速度比較は気が向いたらやるかも。

  2. 公式ドキュメントでは.NET Framework 4.7.2以降と書かれてるけど、nugetを見ると4.6.1以降なら大丈夫っぽい。実際にちゃんと動くことを確認。

  3. XSS対策とのこと。JavaScriptEncoder.UnsafeRelaxedJsonEscapingでは<>&'などもエスケープされないため注意しましょう。

  4. 内部的にUTF-16であるstringを介さずに直接UTF-8のデータとしてStreamへアクセスできるので高速……だと思うんですが未検証。少なくともUtf8JsonWriterは5~10%高速らしい。