System.Text.Json で JsonSerializerOptions インスタンスをインスタンス化する方法

この記事では、JsonSerializerOptions の使用時にパフォーマンスの問題を回避する方法について説明します。 また、使用可能なパラメーター化コンストラクターの使用方法についても説明します。

JsonSerializerOptions インスタンスの再利用

同じオプションで JsonSerializerOptions を繰り返し使用する場合、使用のたびに新しい JsonSerializerOptions インスタンスを作成しないでください。 すべての呼び出しで同じインスタンスを再利用します。 ここでは、カスタム コンバーター用に作成し、JsonSerializer.Serialize または JsonSerializer.Deserialize を呼び出すコードに関して説明しています。 複数のスレッドで同じインスタンスを使用することに問題はありません。 オプション インスタンス上のメタデータ キャッシュはスレッドセーフであり、最初のシリアル化または逆シリアル化を行うと、その後はインスタンスを変更できなくなります。

次のコードでは、新しいオプションのインスタンスを使用する場合にパフォーマンスが低下することを示しています。

using System.Diagnostics;
using System.Text.Json;

namespace OptionsPerfDemo
{
    public record Forecast(DateTime Date, int TemperatureC, string Summary);

    public class Program
    {
        public static void Main()
        {
            Forecast forecast = new(DateTime.Now, 40, "Hot");
            JsonSerializerOptions options = new() { WriteIndented = true };
            int iterations = 100000;

            var watch = Stopwatch.StartNew();
            for (int i = 0; i < iterations; i++)
            {
                Serialize(forecast, options);
            }
            watch.Stop();
            Console.WriteLine($"Elapsed time using one options instance: {watch.ElapsedMilliseconds}");

            watch = Stopwatch.StartNew();
            for (int i = 0; i < iterations; i++)
            {
                Serialize(forecast);
            }
            watch.Stop();
            Console.WriteLine($"Elapsed time creating new options instances: {watch.ElapsedMilliseconds}");
        }

        private static void Serialize(Forecast forecast, JsonSerializerOptions? options = null)
        {
            _ = JsonSerializer.Serialize<Forecast>(
                forecast,
                options ?? new JsonSerializerOptions() { WriteIndented = true });
        }
    }
}

// Produces output like the following example:
//
//Elapsed time using one options instance: 190
//Elapsed time creating new options instances: 40140

上のコードでは、同じオプション インスタンスを使用して、小さいオブジェクトを 100,000 回シリアル化しています。 その後、同じ回数だけ同じオブジェクトがシリアル化され、毎回新しいオプション インスタンスが作成されます。 実行時の典型的な差は、40,140 ミリ秒と比較し 190 です。 イテレーションの回数を増やすと、この差はさらに広がります。

新しいオプション インスタンスが渡されると、オブジェクト グラフ内の各型の最初のシリアル化時にシリアライザーはウォームアップ フェーズになります。 このウォームアップには、シリアル化に必要なメタデータのキャッシュの作成が含まれます。 メタデータには、プロパティのゲッター、セッター、コンストラクターの引数、指定した属性などに対するデリゲートが含まれています。 このメタデータ キャッシュは、オプションのインスタンスに格納されています。 同じウォームアップ プロセスとキャッシュは逆シリアル化にも該当します。

JsonSerializerOptions インスタンスのメタデータのキャッシュのサイズは、シリアル化される型の数によって異なります。 動的に生成された型など、多数の型をシリアライザーに渡すと、キャッシュのサイズは増加し続け、最終的に OutOfMemoryException が発生する可能性があります。

JsonSerializerOptions.Default プロパティ

使用する必要がある JsonSerializerOptions のインスタンスが既定のインスタンスである (既定の設定と既定のコンバーターをすべて備えている) 場合は、オプションのインスタンスを作成するのではなく、JsonSerializerOptions.Default プロパティを使用します。 詳細については、「既定のシステム コンバーターを使用する」を参照してください。

JsonSerializerOptions をコピーする

次の例で示すように、JsonSerializerOptions constructor を使用すると、既存のインスタンスと同じオプションを使用して、新しいインスタンスを作成できます。

using System.Text.Json;

namespace CopyOptions
{
    public class Forecast
    {
        public DateTime Date { get; init; }
        public int TemperatureC { get; set; }
        public string? Summary { get; set; }
    };

    public class Program
    {
        public static void Main()
        {
            Forecast forecast = new()
            {
                Date = DateTime.Now,
                TemperatureC = 40,
                Summary = "Hot"
            };

            JsonSerializerOptions options = new()
            {
                WriteIndented = true
            };

            JsonSerializerOptions optionsCopy = new(options);
            string forecastJson =
                JsonSerializer.Serialize<Forecast>(forecast, optionsCopy);

            Console.WriteLine($"Output JSON:\n{forecastJson}");
        }
    }
}

// Produces output like the following example:
//
//Output JSON:
//{
//  "Date": "2020-10-21T15:40:06.8998502-07:00",
//  "TemperatureC": 40,
//  "Summary": "Hot"
//}
Imports System.Text.Json
Imports System.Text.Json.Serialization

Namespace CopyOptions

    Public Class Forecast
        Public Property [Date] As Date
        Public Property TemperatureC As Integer
        Public Property Summary As String
    End Class

    Public NotInheritable Class Program

        Public Shared Sub Main()
            Dim forecast1 As New Forecast() With {
                .[Date] = Date.Now,
                .Summary = Nothing,
                .TemperatureC = CType(Nothing, Integer)
            }

            Dim options As New JsonSerializerOptions() With {
                .DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault
            }

            Dim optionsCopy As New JsonSerializerOptions
            Dim forecastJson As String = JsonSerializer.Serialize(forecast1, optionsCopy)

            Console.WriteLine($"Output JSON:{forecastJson}")
        End Sub

    End Class

End Namespace

' Produces output like the following example:
'
'Output JSON:
'{
'  "Date": "2020-10-21T15:40:06.8998502-07:00",
'  "TemperatureC": 40,
'  "Summary": "Hot"
'}

既存の JsonSerializerOptions インスタンスのメタデータ キャッシュは、新しいインスタンスにはコピーされません。 そのため、このコンストラクターを使用することは、既存の JsonSerializerOptions のインスタンスを再利用することと同じではありません。

JsonSerializerOptions の Web の既定値

次のオプションでは、Web アプリの既定値が異なります。

.NET 9 以降のバージョンでは、JsonSerializerOptions.Web シングルトンを使って、ASP.NET Core が Web アプリに使う既定のオプションでシリアル化できます。 以前のバージョンでは、次の例に示すように、JsonSerializerOptions コンストラクターを呼び出して、Web 既定値を使って新しいインスタンスを作成します。

using System.Text.Json;

namespace OptionsDefaults
{
    public class Forecast
    {
        public DateTime? Date { get; init; }
        public int TemperatureC { get; set; }
        public string? Summary { get; set; }
    };

    public class Program
    {
        public static void Main()
        {
            Forecast forecast = new()
            {
                Date = DateTime.Now,
                TemperatureC = 40,
                Summary = "Hot"
            };

            JsonSerializerOptions options = new(JsonSerializerDefaults.Web)
            {
                WriteIndented = true
            };

            Console.WriteLine(
                $"PropertyNameCaseInsensitive: {options.PropertyNameCaseInsensitive}");
            Console.WriteLine(
                $"JsonNamingPolicy: {options.PropertyNamingPolicy}");
            Console.WriteLine(
                $"NumberHandling: {options.NumberHandling}");

            string forecastJson = JsonSerializer.Serialize<Forecast>(forecast, options);
            Console.WriteLine($"Output JSON:\n{forecastJson}");

            Forecast? forecastDeserialized =
                JsonSerializer.Deserialize<Forecast>(forecastJson, options);

            Console.WriteLine($"Date: {forecastDeserialized?.Date}");
            Console.WriteLine($"TemperatureC: {forecastDeserialized?.TemperatureC}");
            Console.WriteLine($"Summary: {forecastDeserialized?.Summary}");
        }
    }
}

// Produces output like the following example:
//
//PropertyNameCaseInsensitive: True
//JsonNamingPolicy: System.Text.Json.JsonCamelCaseNamingPolicy
//NumberHandling: AllowReadingFromString
//Output JSON:
//{
//  "date": "2020-10-21T15:40:06.9040831-07:00",
//  "temperatureC": 40,
//  "summary": "Hot"
//}
//Date: 10 / 21 / 2020 3:40:06 PM
//TemperatureC: 40
//Summary: Hot
Imports System.Text.Json

Namespace OptionsDefaults

    Public Class Forecast
        Public Property [Date] As Date
        Public Property TemperatureC As Integer
        Public Property Summary As String
    End Class

    Public NotInheritable Class Program

        Public Shared Sub Main()
            Dim forecast1 As New Forecast() With {
                .[Date] = Date.Now,
                .TemperatureC = 40,
                .Summary = "Hot"
                }

            Dim options As New JsonSerializerOptions(JsonSerializerDefaults.Web) With {
                .WriteIndented = True
                }

            Console.WriteLine(
                $"PropertyNameCaseInsensitive: {options.PropertyNameCaseInsensitive}")
            Console.WriteLine(
                $"JsonNamingPolicy: {options.PropertyNamingPolicy}")
            Console.WriteLine(
                $"NumberHandling: {options.NumberHandling}")

            Dim forecastJson As String = JsonSerializer.Serialize(forecast1, options)
            Console.WriteLine($"Output JSON:{forecastJson}")

            Dim forecastDeserialized As Forecast = JsonSerializer.Deserialize(Of Forecast)(forecastJson, options)

            Console.WriteLine($"Date: {forecastDeserialized.[Date]}")
            Console.WriteLine($"TemperatureC: {forecastDeserialized.TemperatureC}")
            Console.WriteLine($"Summary: {forecastDeserialized.Summary}")
        End Sub

    End Class

End Namespace

' Produces output like the following example:
'
'PropertyNameCaseInsensitive: True
'JsonNamingPolicy: System.Text.Json.JsonCamelCaseNamingPolicy
'NumberHandling: AllowReadingFromString
'Output JSON:
'{
'  "date": "2020-10-21T15:40:06.9040831-07:00",
'  "temperatureC": 40,
'  "summary": "Hot"
'}
'Date: 10 / 21 / 2020 3:40:06 PM
'TemperatureC: 40
'Summary: Hot