概要
本記事は、.NETの標準的なJSONシリアライザ(下記3つ)を次の観点で調査したものです。
1.System.Text.Json
2.Newtonsoft.Json(左記は正式名です。以後はJSON.NETと表記)
3.DataContractJsonSerializer
調査の観点
・「どのJSONシリアライザを選ぶべきか?」
・「パフォーマンスはどうか?」
まとめ
いきなりですが、先にまとめを書きます。
1. System.Text.Jsonを使えるなら、使う
理由は2つある。
①Microsoft社はJSON.NETを共通フレームワークから除こうしている。
.NET Core 3.0の破壊的変更より抜粋
認証:Newtonsoft.Json 型の置き換え
ASP.NET Core 3.0 では、Authentication API で使用される Newtonsoft.Json 型が System.Text.Json 型に置き換えられました。 次の場合を除き、Authentication パッケージの基本的な使用方法は影響を受けません。
dotnet/aspnetcore #7289より引用
As part of the ongoing effort to remove Newtonsoft.Json from the shared framework these types have now been replaced on the Authentication APIs.
(機械翻訳)共有フレームワークからNewtonsoft.Jsonを削除するための継続的な取り組みの一環として、これらのタイプは認証APIで置き換えられました。
②Microsoft社はDataContractJsonSerializerを推奨していない。
Microsoft社:DataContractJsonSerializerのページより引用
JSON へのシリアル化と JSON からの逆シリアル化を含むほとんどのシナリオでは、 名前空間の System.Text.Jsonのapiを使用することをお勧めします。
サポートバージョンの一覧
Microsoft社 JSONシリアル化 概要のライブラリ入手方法より引用
.NET Standard 2.0 以降のバージョン
.NET Framework 4.7.2 以降のバージョン
.NET Core 2.0、2.1、および 2.2
2 System.Text.Jsonを使えない場合、JSON.NETを使う
System.Text.Jsonをサポートしないバージョンの場合は、JSON.NETの使用を考える。
書籍「実践で役立つC#プログラミングのイディオム/定石&パターン」P317 より引用
ASP.NET MVCではNetsoftのJSON.NETが標準でプロジェクトに組み込まれています。そのため、ASP.NET MVCの場合は、DataContractJsonSerializerではなく、JSON.NETを使い、JSON形式のシリアル化/逆シリアル化を行うのが一般的です。
ASP.NET MVCでなくてもJSON.NETを使うことができますので、可能であればJSON.NETの利用も検討してみてください。
JSON.NETの使い方は下記が参考になります。
@IT JSONデータを作成/解析するには?[C#/VB]
3.System.Text.Jsonは使い方次第で、パフォーマンスが低下する
Json文字列を日本語で出力させるために、System.Text.Jsonにエスケープを抑止の設定(※)を追加したところ、抑止の設定を追加しな場合に比べて、パフォーマンスが低下した。
System.Text.Json以外のライブラリと比較しても、抑止設定を追加した場合のパフォーマンスは他に比べて低い。
① 実行速度の低下(下記表のmeanを参照。)
② メモリ使用量の増加(下記表のAllocatedを参照)
※System.Text.Json で文字エンコードをカスタマイズする方法より
既定では、シリアライザーでは ASCII 以外のすべての文字がエスケープされます。
つまり、\uxxxx に置き換えられます。xxxx は文字の Unicode コードです。
Method
Mean
StdDev
Allocated
DataContractJsonSerializer
8.887 us
0.1165 us
14152 B
JSON.Net
3.912 us
0.0457 us
4304 B
SystemTextJson(エスケープ抑止あり)
889.540 us
8.6991 us
44213 B
SystemTextJson(エスケープ抑止なし)
3.106 us
0.0236 us
672 B
この結果は下記の手順を基に作成したものです。
(a) 3つのライブラリを用いて、「同一データに対してシリアライズ→デシリアライズ」を行う関数を作成した。
(b) 前記の関数に、最終的な出力が同じになるように調整を加えた。
(c) benchMarkdotNetで計測を開始した。
(d) 結果を比較する。
動作環境について
.NET Core 3.1.8
BenchmarkDotNet V0.12.1
JSON.NET(Newtonsoft.Json) V13.0.1
OS: Windows 10.0.19041.867
Intel Core i7-7700HQ CPU 2.80GHz
パフォーマンス計測に関して
方針
「計測対象の最終出力結果が同一になる関数」を作成して、それぞれを比較する。
同一の入力データに対して、シリアライズ後にデシリアライズする。
それぞれをコンソール出力した時に下記の通りになるように調整する(※)。
※計測時にはコンソール出力しない。
//調整結果
//シリアライズ後
{"company":"株式会社 自宅警備","department":"開発部","name":"山本太郎","sex":"男","age":30}
//デシリアライズ後
company=株式会社 自宅警備,department=開発部,name=山本太郎,sex=男,age=30
遭遇した問題:System.Text.Jsonが日本語表記されない
シリアライズ後、Unicodeコードで出力される。
//シリアライズ Unicodeを日本語にエンコードできてない
{"Company":"\u682A\u5F0F\u4F1A\u793E \u81EA\u5B85\u8B66\u5099","Department":"\u958B\u767A\u90E8","Name":"\u5C71\u672C\u592A\u90CE","Sex":"\u7537","Age":30}
//デシリアライズ
company=株式会社 自宅警備,department=開発部,name=山本太郎,sex=男,age=30
原因は以下。
System.Text.Json で文字エンコードをカスタマイズする方法より
既定では、シリアライザーでは ASCII 以外のすべての文字がエスケープされます。 つまり、\uxxxx に置き換えられます。xxxx は文字の Unicode コードです。
すべての言語セットをエスケープせずにシリアル化するには、UnicodeRanges.All を使用します。
下記サイトを参考にエスケープを抑止するオプション指定を追加した。
.NET Core:JsonSerializerの実践的な使い方
また、計測の観点にオプション指定の有無を追加することを決めた。
コード
計測対象
using System;
using BenchmarkDotNet.Attributes;
using System.IO;
using System.Runtime.Serialization;
using System.Text;
using System.Text.Json;
using System.Text.Unicode;
using System.Runtime.Serialization.Json;
using System.Text.Encodings.Web;
namespace StudyJsonSerializer
{
[MemoryDiagnoser]
public class MeasurementJsonSerializer
{
/// <summary>
/// DataContractJsonSerializerによるシリアル化/デシリアル化のサンプル
/// </summary>
[Benchmark]
public void UsingDataContractJsonSerializer()
{
//シリアル化
using var serializerStream = new MemoryStream();
var serializer = new DataContractJsonSerializer(OfficeWorkerContract.GetType());
serializer.WriteObject(serializerStream, OfficeWorkerContract);
var jsonString = Encoding.UTF8.GetString(serializerStream.ToArray());
//デシリアル化
var byteArray = Encoding.UTF8.GetBytes(jsonString);
using var deserializerStreamtream = new MemoryStream(byteArray);
var deserializer = new DataContractJsonSerializer(typeof(OfficeWorkerContractModel));
var officeWorker = serializer.ReadObject(deserializerStreamtream) as OfficeWorkerContractModel;
}
/// <summary>
/// Json.NETによるシリアル化/デシリアル化のサンプル
/// </summary>
[Benchmark]
public void UsingJsonNet()
{
var jsonString = Newtonsoft.Json.JsonConvert.SerializeObject(OfficeWorker);
var result = Newtonsoft.Json.JsonConvert.DeserializeObject<OfficeWorker>(jsonString);
}
/// <summary>
/// System.Text.Json.Serializationによるシリアル化/デシリアル化のサンプル
/// </summary>
[Benchmark]
public void UsingSystemTextJson()
{
var options = new JsonSerializerOptions()
{
// すべての言語セットをエスケープせずにシリアル化させる
Encoder = JavaScriptEncoder.Create(UnicodeRanges.All)
};
var jsonString = System.Text.Json.JsonSerializer.Serialize(OfficeWorker, options);
var result = System.Text.Json.JsonSerializer.Deserialize<OfficeWorker>(jsonString);
}
/// <summary>
/// System.Text.Json.Serializationによるシリアル化/デシリアル化のオプション指定を外す
/// </summary>
[Benchmark]
public void UsingSystemTextJsonWithoutSerializeOption()
{
var jsonString = System.Text.Json.JsonSerializer.Serialize(OfficeWorker);
var result = System.Text.Json.JsonSerializer.Deserialize<OfficeWorker>(jsonString);
}
/// <summary>
/// DataContractJsonSerializer用(他と異なるため念の為、分ける)
/// </summary>
private static readonly OfficeWorker OfficeWorker = new OfficeWorker()
{
Company = "株式会社 自宅警備",
Department = "開発部",
Name = "山本太郎",
Sex = "男",
Age = 30,
};
/// <summary>
/// DataContractJsonSerializer以外で使用
/// </summary>
private static readonly OfficeWorkerContractModel OfficeWorkerContract = new OfficeWorkerContractModel()
{
Company = "株式会社 自宅警備",
Department = "開発部",
Name = "山本太郎",
Sex = "男",
Age = 30,
};
}
/// <summary>
/// DataContractJsonSerializer用の会社員モデル(属性指定が必要なため、念の為分ける)
/// </summary>
[DataContract(Name = "officeWorker")]
public class OfficeWorkerContractModel
{
/// <summary>
/// 会社
/// </summary>
[DataMember(Name = "company",Order = 0)]
public string Company { get; set; }
/// <summary>
/// 部署
/// </summary>
[DataMember(Name = "department", Order = 1)]
public string Department { get; set; }
/// <summary>
/// 名前
/// </summary>
[DataMember(Name = "name", Order = 2)]
public string Name { get; set; }
/// <summary>
/// 性別
/// </summary>
[DataMember(Name = "sex", Order = 3)]
public string Sex { get; set; }
/// <summary>
/// 年齢
/// </summary>
[DataMember(Name = "age", Order = 4)]
public uint Age { get; set; }
/// <summary>
/// 文字列変換
/// </summary>
/// <returns></returns>
public override string ToString()
{
return $"company={Company},department={Department},name={Name},sex={Sex},age={Age}";
}
}
/// <summary>
/// 会社員(DataContractJsonSerializer以外で使用)
/// </summary>
public class OfficeWorker
{
/// <summary>
/// 会社
/// </summary>
public string Company { get; set; }
/// <summary>
/// 部署
/// </summary>
public string Department { get; set; }
/// <summary>
/// 名前
/// </summary>
public string Name { get; set; }
/// <summary>
/// 性別
/// </summary>
public string Sex { get; set; }
/// <summary>
/// 年齢
/// </summary>
public uint Age { get; set; }
/// <summary>
/// 文字列変換
/// </summary>
/// <returns></returns>
public override string ToString()
{
return $"company={Company},department={Department},name={Name},sex={Sex},age={Age}";
}
}
}
より正確に測るなら、もっと自前の実装を極力排除すべきである。しかし、そこまでやり切る技量は私にはない。
メイン関数
using BenchmarkDotNet.Running;
namespace StudyJsonSerializer
{
class Program
{
static void Main( string[] args )
{
//計測開始
var summary = BenchmarkRunner.Run<MeasurementJsonSerializer>();
}
}
}
測定結果のサマリ
Method
Mean
Error
StdDev
Gen 0
Gen 1
Gen 2
Allocated
UsingDataContractJsonSerializer
8.887 us
0.1315 us
0.1165 us
4.5013
-
-
14152 B
UsingJsonNet
3.912 us
0.0515 us
0.0457 us
1.3695
-
-
4304 B
UsingSystemTextJson
889.540 us
11.1422 us
8.6991 us
15.6250
7.8125
-
44213 B
UsingSystemTextJsonWithoutSerializeOption
3.106 us
0.0252 us
0.0236 us
0.2136
-
-
672 B
ラベルの説明は下記の通り
Mean : Arithmetic mean of all measurements
Error : Half of 99.9% confidence interval
StdDev : Standard deviation of all measurements
Gen 0 : GC Generation 0 collects per 1000 operations
Gen 1 : GC Generation 1 collects per 1000 operations
Gen 2 : GC Generation 2 collects per 1000 operations
Allocated : Allocated memory per single operation (managed only, inclusive, 1KB = 1024B)
1 us : 1 Microsecond (0.000001 sec)
測定結果の詳細(一応控える)
// * Detailed results *
MeasurementJsonSerializer.UsingDataContractJsonSerializer: DefaultJob
Runtime = .NET Core 3.1.8 (CoreCLR 4.700.20.41105, CoreFX 4.700.20.41903), X64 RyuJIT; GC = Concurrent Workstation
Mean = 8.887 us, StdErr = 0.031 us (0.35%), N = 14, StdDev = 0.117 us
Min = 8.754 us, Q1 = 8.792 us, Median = 8.873 us, Q3 = 8.940 us, Max = 9.203 us
IQR = 0.148 us, LowerFence = 8.570 us, UpperFence = 9.162 us
ConfidenceInterval = [8.756 us; 9.019 us] (CI 99.9%), Margin = 0.131 us (1.48% of Mean)
Skewness = 1.17, Kurtosis = 4.18, MValue = 2
-------------------- Histogram --------------------
[8.691 us ; 8.977 us) | @@@@@@@@@@@@
[8.977 us ; 9.266 us) | @@
---------------------------------------------------
MeasurementJsonSerializer.UsingJsonNet: DefaultJob
Runtime = .NET Core 3.1.8 (CoreCLR 4.700.20.41105, CoreFX 4.700.20.41903), X64 RyuJIT; GC = Concurrent Workstation
Mean = 3.912 us, StdErr = 0.012 us (0.31%), N = 14, StdDev = 0.046 us
Min = 3.848 us, Q1 = 3.878 us, Median = 3.912 us, Q3 = 3.934 us, Max = 4.004 us
IQR = 0.056 us, LowerFence = 3.795 us, UpperFence = 4.018 us
ConfidenceInterval = [3.860 us; 3.963 us] (CI 99.9%), Margin = 0.052 us (1.32% of Mean)
Skewness = 0.32, Kurtosis = 2.08, MValue = 2
-------------------- Histogram --------------------
[3.823 us ; 4.029 us) | @@@@@@@@@@@@@@
---------------------------------------------------
MeasurementJsonSerializer.UsingSystemTextJson: DefaultJob
Runtime = .NET Core 3.1.8 (CoreCLR 4.700.20.41105, CoreFX 4.700.20.41903), X64 RyuJIT; GC = Concurrent Workstation
Mean = 889.540 us, StdErr = 2.511 us (0.28%), N = 12, StdDev = 8.699 us
Min = 875.936 us, Q1 = 883.029 us, Median = 890.843 us, Q3 = 895.243 us, Max = 904.026 us
IQR = 12.213 us, LowerFence = 864.709 us, UpperFence = 913.563 us
ConfidenceInterval = [878.398 us; 900.682 us] (CI 99.9%), Margin = 11.142 us (1.25% of Mean)
Skewness = -0.15, Kurtosis = 1.7, MValue = 2
-------------------- Histogram --------------------
[870.949 us ; 909.013 us) | @@@@@@@@@@@@
---------------------------------------------------
MeasurementJsonSerializer.UsingSystemTextJsonWithoutSerializeOption: DefaultJob
Runtime = .NET Core 3.1.8 (CoreCLR 4.700.20.41105, CoreFX 4.700.20.41903), X64 RyuJIT; GC = Concurrent Workstation
Mean = 3.106 us, StdErr = 0.006 us (0.20%), N = 15, StdDev = 0.024 us
Min = 3.052 us, Q1 = 3.097 us, Median = 3.104 us, Q3 = 3.122 us, Max = 3.142 us
IQR = 0.025 us, LowerFence = 3.060 us, UpperFence = 3.160 us
ConfidenceInterval = [3.081 us; 3.131 us] (CI 99.9%), Margin = 0.025 us (0.81% of Mean)
Skewness = -0.49, Kurtosis = 2.76, MValue = 2
-------------------- Histogram --------------------
[3.039 us ; 3.155 us) | @@@@@@@@@@@@@@@
---------------------------------------------------
執筆の際に参考にしたサイト/書籍
.NET Core 3.0の破壊的変更
dotnet/aspnetcore #7289
Microsoft社:DataContractJsonSerializer
Microsoft社 JSONシリアル化 概要
書籍:実践で役立つC#プログラミングのイディオム/定石&パターン 12.3 JSONデータのシリアル化と逆シリアル化
@IT JSONデータを作成/解析するには?[C#/VB]
System.Text.Json で文字エンコードをカスタマイズする方法
.NET 内で JSON のシリアル化と逆シリアル化 (マーシャリングとマーシャリングの解除) を行う方法
.NET Core:JsonSerializerの実践的な使い方
Newtonsoft.Json と System.Text.Json の相違点の表
【.NET/C#】メソッドのパフォーマンスを簡単に集計するライブラリの紹介
↧