【C#】JSONのシリアライザは、System.Text.JSONを使おう。

# 概要
本記事は、.NETの標準的なJSONシリアライザ(下記3つ)を次の観点で調査したものです。
1.System.Text.Json
2.Newtonsoft.Json(左記はNugetに表示される名称です。以後はJSON.NETと表記)
3.DataContractJsonSerializer

調査の観点
・「どのJSONシリアライザを選ぶべきか？」
・「パフォーマンスはどうか？」

# まとめ
いきなりですが、先にまとめを書きます。

## 1. System.Text.Jsonを使おう
理由は２つある。
**①Microsoft社はJSON.NETを共通フレームワークから除こうしている。**

[.NET Blog: Try the new System.Text.Json APIs](https://devblogs.microsoft.com/dotnet/try-the-new-system-text-json-apis/)より抜粋
><b>ASP.NETCoreからJson.NETの依存関係を削除します。</b><br>現在、ASP.NETCoreはJson.NETに依存しています。これにより、ASP.NET CoreとJson.NETが緊密に統合されますが、Json.NETのバージョンが基盤となるプラットフォームによって決定されることも意味します。ただし、Json.NETは頻繁に更新され、アプリケーション開発者は特定のバージョンを使用したい、または使用しなければならないことがよくあります。したがって、ASP.NET Core 3.0からJson.NETの依存関係を削除して、お客様が誤って基盤となるプラットフォームを壊してしまうことを恐れずに、使用するバージョンを選択できるようにします。

[.NET Core 3.0の破壊的変更](https://docs.microsoft.com/ja-jp/dotnet/core/compatibility/3.0#authentication-newtonsoftjson-types-replaced)より抜粋

>認証:Newtonsoft.Json 型の置き換え
ASP.NET Core 3.0 では、Authentication API で使用される Newtonsoft.Json 型が System.Text.Json 型に置き換えられました。 次の場合を除き、Authentication パッケージの基本的な使用方法は影響を受けません。

[dotnet/aspnetcore #7289](https://github.com/dotnet/aspnetcore/issues/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](https://docs.microsoft.com/ja-jp/dotnet/api/system.runtime.serialization.json.datacontractjsonserializer?view=netcore-3.1)のページより引用
>JSON へのシリアル化と JSON からの逆シリアル化を含むほとんどのシナリオでは、 名前空間の System.Text.Jsonのapiを使用することをお勧めします。

**サポートバージョンの一覧**
[Microsoft社 JSONシリアル化 概要](https://docs.microsoft.com/ja-jp/dotnet/standard/serialization/system-text-json-overview)のライブラリ入手方法より引用
> .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］](https://www.atmarkit.co.jp/ait/articles/1709/13/news018.html)

## 3.System.Text.Jsonは使い方に気をつけよう
使い方次第で、パフォーマンスが低下することがある。
Json文字列を日本語で出力させるために、System.Text.Jsonにエスケープを抑止の設定(※)を追加したところ、抑止の設定を追加しな場合に比べて、パフォーマンスが低下した。
System.Text.Json以外のライブラリと比較しても、抑止設定を追加した場合のパフォーマンスは他に比べて低い。
① 実行速度の低下(下記表のmeanを参照。)
② メモリ使用量の増加(下記表のAllocatedを参照)

つまり、ASCII以外の文字列を使わなければ、高いパフォーマンスに使える。少なくともUnicodeでエスケープされることは頭の片隅においた方が良い。

※[System.Text.Json で文字エンコードをカスタマイズする方法](https://docs.microsoft.com/ja-jp/dotnet/standard/serialization/system-text-json-character-encoding)より
>**既定では、シリアライザーでは 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 で文字エンコードをカスタマイズする方法](https://docs.microsoft.com/ja-jp/dotnet/standard/serialization/system-text-json-character-encoding)より
>既定では、シリアライザーでは ASCII 以外のすべての文字がエスケープされます。 つまり、\uxxxx に置き換えられます。xxxx は文字の Unicode コードです。
>すべての言語セットをエスケープせずにシリアル化するには、UnicodeRanges.All を使用します。

下記サイトを参考にエスケープを抑止するオプション指定を追加した。
[.NET Core:JsonSerializerの実践的な使い方](https://www.nextdoorwith.info/wp/se/net-core-jsonserializer-how-to-use-practical-example/)

また、計測の観点にオプション指定の有無を追加することを決めた。


## コード

### 計測対象

~~~C#
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}";
        }
    }
}

~~~

より正確に測るなら、もっと自前の実装を極力排除すべきである。しかし、そこまでやり切る技量は私にはない。

### メイン関数

~~~C#
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の破壊的変更](https://docs.microsoft.com/ja-jp/dotnet/core/compatibility/3.0#authentication-newtonsoftjson-types-replaced)
[dotnet/aspnetcore #7289](https://github.com/dotnet/aspnetcore/issues/7289)
[Microsoft社：DataContractJsonSerializer](https://docs.microsoft.com/ja-jp/dotnet/api/system.runtime.serialization.json.datacontractjsonserializer?view=netcore-3.1)
[Microsoft社 JSONシリアル化 概要](https://docs.microsoft.com/ja-jp/dotnet/standard/serialization/system-text-json-overview)
[書籍：実践で役立つC#プログラミングのイディオム/定石＆パターン　12.3 JSONデータのシリアル化と逆シリアル化](https://www.amazon.co.jp/%E5%AE%9F%E6%88%A6%E3%81%A7%E5%BD%B9%E7%AB%8B%E3%81%A4-C-%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E3%81%AE%E3%82%A4%E3%83%87%E3%82%A3%E3%82%AA%E3%83%A0-%E5%AE%9A%E7%9F%B3-%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3/dp/4774187585)
[@IT JSONデータを作成／解析するには？［C#／VB］](https://www.atmarkit.co.jp/ait/articles/1709/13/news018.html)
[System.Text.Json で文字エンコードをカスタマイズする方法](https://docs.microsoft.com/ja-jp/dotnet/standard/serialization/system-text-json-character-encoding)
[.NET 内で JSON のシリアル化と逆シリアル化 (マーシャリングとマーシャリングの解除) を行う方法](https://docs.microsoft.com/ja-jp/dotnet/standard/serialization/system-text-json-how-to?pivots=dotnet-5-0)
[.NET Core:JsonSerializerの実践的な使い方](https://www.nextdoorwith.info/wp/se/net-core-jsonserializer-how-to-use-practical-example/)
[Newtonsoft.Json と System.Text.Json の相違点の表](https://docs.microsoft.com/ja-jp/dotnet/standard/serialization/system-text-json-migrate-from-newtonsoft-how-to?pivots=dotnet-5-0#table-of-differences-between-newtonsoftjson-and-systemtextjson)
[【.NET/C#】メソッドのパフォーマンスを簡単に集計するライブラリの紹介](https://qiita.com/SY81517/items/79f6c5905e758279831a)