Tecnologia
O quão performático o System.Text.Json é em relação ao Newtonsoft.Json
5 minutos de leitura
No mundo das aplicações distribuídas, a integração entre microsserviços diferentes torna-se cada vez mais comum no dia a dia do desenvolvimento de software. O formato JSON (JavaScript Object Notation) é o padrão mais utilizado para a troca de informações entre microsserviços, o que exige a serialização e desserialização das informações trocadas em formato JSON. Em C#, há duas bibliotecas amplamente utilizadas para lidar com JSON: o Newtonsoft.Json, que é bastante robusta, e o System.Text.Json, que é uma alternativa mais recente ao Newtonsoft.Json e oferece uma performance mais leve. Apesar de haver uma nova opção disponível, o Newtonsoft.Json ainda é muito popular entre muitos desenvolvedores .NET e é, de longe, o pacote mais baixado no Nuget.org, conforme pode ser visto na Figura 1.
Enquanto o Newtonsft.Json é mantido por terceiros, o System.Text.Json é mantido pelo time de desenvolvimento do .NET e pode ser usada através do namespace System.Text.Json. Estando ela disponível nativamente a partir do .NET Core 3.1 e suas versões posteriores. Sendo ela também compatível com o .NET Standard 2.0, .NET Framework 4.7.2, .NET Core 2.0, 2.1 e 2.2, através da instalação do pacote System.Text.Json via Nuget.
Mas e o quão performático é o System.Text.Json em relação ao Newtonsoft.Json?
Essa pergunta pode ser respondida com a criação de um Benchmark que compare as duas bibliotecas para serialização e desserialização de JSON.
A biblioteca BenchmarkDotNet irá nos auxiliar nesse comparativo, sendo ela amplamente utilizada pelos times de desenvolvimento do próprio .NET em diversos projetos como: ASP.NET Core, Entity Framework Core, além também de ser utilizada pela equipe do Dapper, Serilog, Autofac e MediatR. Inclusive, ela foi usada no artigo Teste de performance de filtros globais para tratamento de exception em projetos feitos em NET Core.
A ideia será executar uma aplicação do tipo console que execute a serialização usando o Newtonsoft.Json e System.Text.Json de uma lista de produtos.
A versão do .NET escolhida é a 7. Será necessário instalar dois pacotes, o BenchmarkDotNet e o Newtonsoft.Json nas versões mais recentes.
Para fazer a comparação entre as duas abordagens, vamos criar uma classe chamada JsonComparisons. Nela vamos ter dois campos somente leitura, uma para configurar a serialização padrão como Web do System.Text.Json e outra que irá representar a lista de produtos. E também teremos uma propriedade para definir o tamanho da lista de produtos a ser usado para comparação nos Benchmark. E por último, teremos dois métodos que servirão para executar a serialização em cada uma das abordagens, ficando da seguinte forma:
[MemoryDiagnoser]
[RankColumn]
public class JsonComparisons
{
private readonly JsonSerializerOptions _jsonOptions;
private readonly List<Produto> _produtos;
[Params(10, 100, 1_000, 10_000, 100_000, 1_000_000)]
public int TamanhoLista { get; set; }
public JsonComparisons()
{
_jsonOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web);
_produtos = ConstruirProdutos(TamanhoLista);
}
private List<Produto> ConstruirProdutos(int count)
{
return Enumerable.Range(1, count)
.Select(i => new Produto()
{
Id = i,
Nome =
quot;Nome {i}",
Categoria =
quot;Categoria {i}",
Descricao =
quot;Descrição {i}",
Preco = i
})
.ToList();
}
[Benchmark]
public string Newtonsoft() => JsonConvert.SerializeObject(_produtos);
[Benchmark]
public string SystemTextJson() => System.Text.Json.JsonSerializer.Serialize(_produtos, _jsonOptions);
}
Por fim, é alterar o método Main da classe Program para executar o benchmark para a classe de comparação das abordagens de serialização de JSON, ficando da seguinte forma:
public class Program
{
static void Main(string[] args)
{
BenchmarkRunner.Run<JsonComparisons>();
}
}
Para executar o Benchmark, basta abrir o terminal na pasta onde está salvo o csproj do projeto criado e executar usando o CLI (command-line interface) do .NET da seguinte maneira.
dotnet run -c Release
Ao final da execução, teremos uma tabela com os resultados conforme Figura 2.
Nela temos as seguintes informações:
- Mean: Média aritmética de todas as medições
- Error: Metade do intervalo de confiança de 99,9%
- StdDev: Desvio padrão de todas as medições
- Rank: Posição relativa da média do benchmark atual entre todos os benchmarks
- Gen0: A geração 0 do GC coleta por 1.000 operações
- Gen1: A geração 1 do GC coleta por 1.000 operações
- Allocated: Memória alocada por operação única (somente gerenciada, inclusive, 1KB = 1024B)
- 1 us: 1 Microsecond (0.000001 sec)
Com base nos resultados retornados, podemos dizer que o System.Text.Json é mais rápido do que o Newtonsoft.Json. Para provar isso, podemos calcular a diferença percentual de desempenho entre os dois métodos usando a fórmula ((Newtonsoft – SystemTextJson) / Newtonsoft) * 100, onde teremos os seguintes resultados para todos tamanhos de lista de produtos usados no Benchmark:
- TamanhoLista = 10: ((376.3 – 196.5) / 376.3) * 100 = 47,28%
- TamanhoLista = 100: ((402.7 – 196.2) / 402.7) * 100 = 51,19%
- TamanhoLista = 1000: ((371.3 – 203.5) / 371.3) * 100 = 45,69%
- TamanhoLista = 10000: ((379.5 – 189.7) / 379.5) * 100 = 49,97%
- TamanhoLista = 100000: ((372.7 – 196.9) / 372.7) * 100 = 47,13%
- TamanhoLista = 1000000: ((424.5 – 199.4) / 424.5) * 100 = 53,03%
Portanto, podemos concluir que, em média, o SystemTextJson é cerca de 48,5% mais rápido do que o Newtonsoft.Json na desserialização de objetos.
Tem como ser melhor ainda?
Sim, e a abordagem consiste em utilizar uma sobrecarga do Serialize do JsonSerializer, que nos permite passar o Metadata do tipo a ser convertido. Para isso, é necessário criar uma classe parcial de geração de código que herde do JsonSerializerContext e seja decorada com os atributos JsonSourceGenerationOptions e JsonSerializable, como mostrado no código abaixo:
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
[JsonSerializable(typeof(List<Produto>))]
public partial class ProdutoGenerationContext : JsonSerializerContext { }
Ao final, adicione método de Benchmark na classe JsonComparsion que irá testar a abordagem de geração de código a ser usado na serialização com o System.Text.Json.
[Benchmark]
public string SourceGenerator() => System.Text.Json.JsonSerializer.Serialize(_produtos, ProdutoGenerationContext.Default.ListProduto);
Agora basta executar novamente o projeto de benchmark para coletarmos os resultados de performance a respeito das três abordagens. E conforme a Figura 3 podemos ver que usar uma abordagem de geração de código é ainda mais performático.
Com esse Benchmark, podemos concluir que o System.Text.Json é mais performático, principalmente quando usado com a estratégia de geração de código.
Em relação ao método Newtonsoft:
- Diferença de desempenho: 369.3 – 135.1 = 234.2
- Diferença percentual: (234.2 / 135.1) x 100% = 173.35%
Em relação ao método SystemTextJson:
- Diferença de desempenho: 182.7 – 135.1 = 47.6
- Diferença percentual: (47.6 / 135.1) x 100% = 35.23%
Portanto, em relação ao método Newtonsoft, o SourceGenerator é cerca de 173.35% mais rápido em média, e em relação ao método SystemTextJson, o SourceGenerator é cerca de 35.23% mais rápido em média.
Conclusão
Com as comparações feitas, é notório que a performance do SystemTextJson é superior ao NewtonSoft.Json, especialmente quando usamos uma abordagem de geração de código, que chegou a ser 3 vezes mais performático que o Newtonsoft.Json.
Mas é importante levar em consideração as compatibilidades de funcionalidades entre o System.Text.Json e o Newtonsoft.Json antes de migrar. Você pode ver uma lista completa entre as diferenças na própria documentação do .NET através do link https://learn.microsoft.com/pt-br/dotnet/standard/serialization/system-text-json/migrate-from-newtonsoft?pivots=dotnet-7-0#table-of-differences-between-newtonsoftjson-and-systemtextjson
O código fonte completo para esse benchmark pode ser baixado no github (https://github.com/pablotdv/JsonBenchmark).