Como instanciar as instâncias de JsonSerializerOptions com System.Text.Json

Este artigo explica como evitar problemas de desempenho ao usar JsonSerializerOptions. Ele também mostra como usar os construtores parametrizados disponíveis.

Reutilizar instâncias de JsonSerializerOptions

Se você usar JsonSerializerOptions repetidamente com as mesmas opções, não crie uma instância JsonSerializerOptions sempre que a usar. Reutilize a mesma instância para cada chamada. Essa orientação se aplica ao código que você escreve para conversores personalizados e ao chamar JsonSerializer.Serialize ou JsonSerializer.Deserialize. É seguro usar a mesma instância em vários threads. Os caches de metadados na instância de opções são thread-safe, e a instância é imutável após a primeira serialização ou desserialização.

O código a seguir demonstra a penalidade de desempenho por usar novas instâncias de opções.

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

O código anterior serializa um objeto pequeno 100.000 vezes usando a mesma instância de opções. Em seguida, ele serializa o mesmo objeto o mesmo número de vezes e cria uma nova instância de opções a cada vez. Uma diferença típica de tempo de execução é 190 comparado com 40.140 milissegundos. A diferença será ainda maior se você aumentar o número de iterações.

O serializador passa por uma fase de aquecimento durante a primeira serialização de cada tipo no grafo de objeto quando uma nova instância de opções é passada para ele. Esse aquecimento inclui a criação de um cache de metadados necessário para a serialização. Os metadados incluem delegados para getters de propriedade, setters, argumentos de construtor, atributos especificados e assim por diante. Esse cache de metadados é armazenado na instância de opções. O mesmo processo de aquecimento e cache se aplicam à desserialização.

O tamanho do cache de metadados em uma instância JsonSerializerOptions depende do número de tipos a serem serializados. Se você passar vários tipos, por exemplo, tipos gerados dinamicamente, para o serializador, o tamanho do cache continuará crescendo e poderá acabar causando um OutOfMemoryException.

A propriedade JsonSerializerOptions.Default

Se a instância de JsonSerializerOptions da qual você precisa usar for a instância padrão (tem todas as configurações padrão e os conversores padrão), use a propriedade JsonSerializerOptions.Default em vez de criar uma instância de opções. Para obter mais informações, consulte Usar conversor de sistema padrão.

Copiar JsonSerializerOptions

Há um construtor JsonSerializerOptions que permite criar uma nova instância com as mesmas opções de uma instância existente, conforme mostrado no exemplo a seguir:

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"
'}

O cache de metadados da instância existente JsonSerializerOptions não é copiado para a nova instância. Portanto, usar esse construtor não é o mesmo que reutilizar uma instância existente de JsonSerializerOptions.

Padrões da Web para JsonSerializerOptions

As seguintes opções têm padrões diferentes para aplicativos Web:

No .NET 9 e versões posteriores, você pode usar o singleton JsonSerializerOptions.Web para serializar com as opções padrão que o ASP.NET Core usa para aplicativos Web. Em versões anteriores, chame o construtor JsonSerializerOptions para criar uma nova instância com os padrões da Web, conforme mostrado no exemplo a seguir:

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