LoginSignup
15
10

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-04-19

概要

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

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

まとめ

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

1. System.Text.Jsonを使おう

理由は2つある。
①Microsoft社はJSON.NETを共通フレームワークから除こうしている。

.NET Blog: Try the new System.Text.Json APIsより抜粋

ASP.NETCoreからJson.NETの依存関係を削除します。
現在、ASP.NETCoreはJson.NETに依存しています。これにより、ASP.NET CoreとJson.NETが緊密に統合されますが、Json.NETのバージョンが基盤となるプラットフォームによって決定されることも意味します。ただし、Json.NETは頻繁に更新され、アプリケーション開発者は特定のバージョンを使用したい、または使用しなければならないことがよくあります。したがって、ASP.NET Core 3.0から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を参照)

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

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#】メソッドのパフォーマンスを簡単に集計するライブラリの紹介

15
10
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
10