Migrar de Newtonsoft.Json 13 para System.Text.Json em uma base de código grande de .NET 11
Um guia com versões fixadas para trocar o Newtonsoft.Json 13.0.4 pelo System.Text.Json embutido no .NET 11: os mapeamentos de atributos e opções, os valores padrão que mudam seu formato de saída silenciosamente, uma estratégia de implantação em etapas, a verificação e os problemas que afetam bases de código grandes.
Trocar o Newtonsoft.Json pelo System.Text.Json em uma base de código grande raramente é um trabalho de localizar e substituir. As duas bibliotecas discordam nos valores padrão de maneiras que mudam sua saída serializada e quebram a desserialização silenciosamente, então uma troca ingênua envia uma mudança de contrato para cada consumidor do seu JSON. Reserve alguns dias para um serviço pequeno e de duas a quatro semanas para uma base de código extensa com conversores personalizados, payloads polimórficos e análise com dynamic/JObject. O ganho é real: System.Text.Json vem de fábrica com o runtime, serializa cerca de duas vezes mais rápido com uma fração das alocações, e é o único dos dois que roda sob Native AOT. Este artigo fixa Newtonsoft.Json 13.0.4 (a versão estável atual, lançada em 2025-12-30) como origem e o System.Text.Json embutido no .NET 11 SDK com C# 14 como destino. Se você ainda está decidindo se vai migrar ou não, leia primeiro System.Text.Json vs Newtonsoft.Json em 2026; este artigo assume que você já decidiu migrar.
Por que migrar agora
System.Text.Jsonfaz parte do framework compartilhado no .NET 11. Remover oPackageReferencedoNewtonsoft.Jsonelimina uma dependência transitiva que o runtime, o ASP.NET Core e a plataforma de testes têm descartado ativamente.- Throughput. Em payloads POCO típicos, o
System.Text.Jsonserializa cerca de 2x mais rápido que oNewtonsoft.Jsoncom alocações marcadamente menores, porque trabalha diretamente sobre bytes UTF-8 comUtf8JsonReadereUtf8JsonWriterem vez de passar porstringeTextReader. - Native AOT e trimming.
Newtonsoft.Jsondepende de reflexão e não funciona sob Native AOT.System.Text.Jsontem um modo de gerador de código-fonte (JsonSerializerContext) que emite metadados de serialização compatíveis com AOT e seguros para o trimming em tempo de compilação. Se Native AOT está no seu roadmap, esta migração é um pré-requisito, não uma otimização. - Postura de segurança.
System.Text.Jsoné estrito por padrão (RFC 8259), faz escape de caracteres sensíveis a HTML e não ASCII na saída, e não interpreta JSON malformado. Isso elimina uma classe de surpresas de injeção e análise que os valores padrão permissivos doNewtonsoft.Jsonpermitem.
O que quebra
O perigo desta migração não é o código que não compila. É o código que compila bem e muda seu formato de saída. Esta tabela é a que deve ser lida duas vezes.
| Área | Mudança | Severidade |
|---|---|---|
| Correspondência de nomes de propriedade | Newtonsoft.Json não diferencia maiúsculas na leitura por padrão; System.Text.Json diferencia | alta |
| Comentários e vírgulas finais | Aceitos por padrão no Newtonsoft.Json, lançam JsonException no System.Text.Json | alta |
| JSON com aspas simples / sem aspas | Aceito pelo Newtonsoft.Json, rejeitado por design no System.Text.Json | alta |
Valor não-string em propriedade string | Newtonsoft.Json converte 1 ou true; System.Text.Json lança exceção | alta |
| Números entre aspas | Newtonsoft.Json lê "23" em um int; System.Text.Json precisa de NumberHandling | média |
| Escape de caracteres | System.Text.Json faz escape de forma mais agressiva, então os bytes de saída diferem para não-ASCII e HTML | média |
[JsonProperty("name")] | Vira [JsonPropertyName("name")]; não há opções combinadas de ignore/required em um atributo | média |
TypeNameHandling.All | Não há equivalente, por design. O polimorfismo usa [JsonDerivedType] em vez disso | alta |
JObject / JToken / dynamic | Substituídos por JsonNode / JsonDocument / JsonElement com uma API diferente | média |
JsonConvert.PopulateObject | Não há equivalente embutido; precisa de um conversor personalizado ou mesclagem manual | média |
ReferenceLoopHandling.Ignore | Não há modo “descartar o loop silenciosamente”; você tem ReferenceHandler.Preserve ou redesenha o grafo | média |
DateFormatString, DateTimeZoneHandling | Não há controle global de formato de data; precisa de um JsonConverter<DateTime> personalizado | média |
| Precedência de registro de conversores | A coleção Converters agora sobrepõe um atributo no nível do tipo (invertido em relação ao Newtonsoft.Json) | baixa |
A referência autoritativa para cada linha aqui é o guia de migração da Microsoft, que também lista o punhado de recursos (consultas JsonPath, TypeNameHandling.All, análise de aspas simples) que não têm solução alternativa.
Lista de verificação prévia
Faça tudo isso antes de apagar um único using Newtonsoft.Json.
- Fixe o contrato. Se o seu JSON cruza um limite de processo (uma API pública, uma fila de mensagens, uma coluna persistida), capture amostras de referência da saída atual. Serialize um conjunto representativo de objetos com
Newtonsoft.Json13.0.4 e guarde as strings como fixtures de teste. Elas são seu oráculo de regressão. - Inventarie a superfície. Use grep em tudo que toca a biblioteca antiga para conhecer o tamanho do trabalho:
Verifique: a lista de arquivos corresponde ao seu modelo mental de onde a serialização vive. Surpresas aqui (um conversor enterrado em um auxiliar de log) são exatamente o que você quer encontrar agora.# run from the repo root; counts the call sites you have to touch grep -rEl "Newtonsoft\.Json|JsonConvert|JObject|JArray|JToken|JsonProperty|JsonSerializerSettings" --include="*.cs" . - Sinalize os recursos sem solução alternativa. Procure especificamente por
TypeNameHandling,SelectToken,SelectTokense fixtures de teste com aspas simples. Se encontrarTypeNameHandling.Allou.Auto, pare e projete a substituição do polimorfismo antes de continuar, porque não há substituto direto para isso. - Confirme o destino. Execute
dotnet --versione confirme11.0.x.System.Text.Jsonvem embutido, então não há pacote a adicionar para os cenários principais; você só adicionaSystem.Text.Jsoncomo umPackageReferenceexplícito se precisar de uma versão out-of-band mais nova do que a que o SDK inclui.
Passos de migração
-
Mapeie as opções globais.
Newtonsoft.Jsoncentraliza o comportamento emJsonSerializerSettings.System.Text.JsonusaJsonSerializerOptions. Traduza seu objeto de configuração existente campo por campo; não aceite os valores padrão doSystem.Text.Jsoncegamente, porque eles diferem do que seu código vem emitindo há anos.// .NET 11, C# 14 // BEFORE: Newtonsoft.Json 13.0.4 var settings = new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver(), NullValueHandling = NullValueHandling.Ignore, ReferenceLoopHandling = ReferenceLoopHandling.Ignore, }; // AFTER: System.Text.Json (in-box on .NET 11) var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, PropertyNameCaseInsensitive = true, // restore Newtonsoft read behavior DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, ReferenceHandler = ReferenceHandler.IgnoreCycles, // closest match to ReferenceLoopHandling.Ignore // AllowTrailingCommas = true, // uncomment only if your inputs have them // ReadCommentHandling = JsonCommentHandling.Skip, // uncomment only if your inputs have comments };Verifique: serialize seus objetos de amostra de referência com estas
optionse compare com os fixtures do passo 1 do preflight. A diferença deve estar vazia ou explicada.PropertyNameCaseInsensitive = trueé a linha mais importante para bases de código grandes, porque inúmeros caminhos de desserialização dependem silenciosamente da correspondência sem diferenciar maiúsculas doNewtonsoft.Json. -
Substitua os atributos.
A renomeação de atributos é mecânica, mas
[JsonProperty]empacotava várias responsabilidades em um único atributo que oSystem.Text.Jsonsepara.// .NET 11, C# 14 // BEFORE public class Order { [JsonProperty("order_id")] public int Id { get; set; } [JsonProperty("notes", NullValueHandling = NullValueHandling.Ignore)] public string? Notes { get; set; } [JsonProperty(Required = Required.Always)] public string Customer { get; set; } = ""; [JsonIgnore] public string Internal { get; set; } = ""; } // AFTER public class Order { [JsonPropertyName("order_id")] public int Id { get; set; } [JsonPropertyName("notes")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Notes { get; set; } [JsonRequired] // or the C# `required` modifier public string Customer { get; set; } = ""; [JsonIgnore] public string Internal { get; set; } = ""; }Verifique: o projeto compila com zero diretivas
usingdeNewtonsoft.Jsonno assembly de modelos, e um teste de ida e volta (Deserialize(Serialize(order))) preserva cada campo. -
Porte os conversores personalizados.
É aqui que as horas vão embora. As formas são similares mas os contratos diferem: os conversores do
System.Text.Jsontrabalham sobreUtf8JsonReader(umref struct) eUtf8JsonWriter, eReadé chamado posicionado no primeiro token.// .NET 11, C# 14 -- a converter that reads/writes DateTime in a fixed format, // replacing Newtonsoft's DateFormatString / DateTimeZoneHandling settings. public sealed class Iso8601DateTimeConverter : JsonConverter<DateTime> { public override DateTime Read(ref Utf8JsonReader reader, Type type, JsonSerializerOptions o) => DateTime.ParseExact(reader.GetString()!, "yyyy-MM-dd'T'HH:mm:ss'Z'", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal); public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions o) => writer.WriteStringValue(value.ToUniversalTime().ToString("yyyy-MM-dd'T'HH:mm:ss'Z'", CultureInfo.InvariantCulture)); }Registre-o em
options.Converters, não apenas como um atributo de tipo, e note a mudança de precedência: noSystem.Text.Jsonum conversor na coleçãoConverterssobrepõe um atributo[JsonConverter]no nível do tipo, o contrário doNewtonsoft.Json. Os detalhes mecânicos estão em como escrever um JsonConverter personalizado em System.Text.Json. Verifique cada conversor portado contra seu próprio fixture, não apenas o payload de ponta a ponta, para que uma surpresa de precedência não se esconda atrás de um teste de integração que passa. -
Substitua o polimorfismo e o TypeNameHandling.
Se você usava
TypeNameHandlingpara fazer ida e volta de uma hierarquia de classes, não há equivalente, e isso é deliberado:TypeNameHandling.Allé um vetor de execução remota de código bem conhecido.System.Text.Jsonfaz polimorfismo discriminado com atributos no tipo base.// .NET 11, C# 14 [JsonDerivedType(typeof(Dog), typeDiscriminator: "dog")] [JsonDerivedType(typeof(Cat), typeDiscriminator: "cat")] public abstract class Animal { public string Name { get; set; } = ""; } public sealed class Dog : Animal { public bool GoodBoy { get; set; } } public sealed class Cat : Animal { public int Lives { get; set; } }Isso emite um discriminador
"$type": "dog"e o lê de volta para o subtipo correto. Verifique: serialize umaList<Animal>de subtipos misturados, desserialize-a e confirme que os tipos em tempo de execução sobrevivem. Note que o formato de saída mudou (uma string discriminadora explícita em vez do$typequalificado por assembly doNewtonsoft.Json), então qualquer consumidor externo deve ser atualizado em conjunto. -
Converta a análise com dynamic e JObject.
O código que mexe em JSON sem tipo via
JObject/JToken/dynamicpassa paraJsonNode(mutável) ouJsonDocument/JsonElement(somente leitura, em pool).// .NET 11, C# 14 // BEFORE: JObject o = JObject.Parse(json); var name = (string)o["user"]!["name"]!; JsonNode root = JsonNode.Parse(json)!; string name = root["user"]!["name"]!.GetValue<string>();A única armadilha:
JsonDocumentpossui um buffer em pool e éIDisposable, diferente deJObject. Envolva-o em umusingou você vazará o buffer alugado. PrefiraJsonNodequando precisar de uma árvore mutável parecida comJObject. Verifique: cada antigo caminho de acesso aJObjecttem um teste unitário que exercita as mesmas buscas de chave. -
Troque a integração do ASP.NET Core.
Se a base de código chama
AddNewtonsoftJson()noProgram.cs, removê-lo comuta todo o pipeline paraSystem.Text.Json. Os valores padrão web do ASP.NET Core já habilitam camelCase, a correspondência sem diferenciar maiúsculas e a leitura de números entre aspas, então muitas das suas opções manuais se tornam redundantes no caminho MVC.// .NET 11, C# 14 // BEFORE: builder.Services.AddControllers().AddNewtonsoftJson(); builder.Services.AddControllers().AddJsonOptions(o => { o.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; // camelCase + case-insensitive are already on by ASP.NET Core web defaults });Atenção ao limite de profundidade: o ASP.NET Core limita o
MaxDepthdoSystem.Text.Jsona 32, não ao padrão da biblioteca de 64. Payloads profundamente aninhados que funcionavam comAddNewtonsoftJson()podem começar a lançar exceções. Verifique: execute os testes de integração do controller e confirme que nenhum payload ultrapassa o limite de profundidade.
Verificação
Execute esta lista de testes de fumaça após cada PR, não apenas no final:
- A solução compila com zero referências a
Newtonsoft.Jsonnos projetos migrados (grep -r "Newtonsoft" --include="*.csproj"não retorna nada para esses projetos). - A diferença de amostras de referência do passo 1 do preflight está vazia ou cada diferença está documentada e é intencional.
- Toda a suíte de testes passa:
dotnet testreporta zero falhas. - Um teste de ida e volta (
Deserialize(Serialize(x))) é válido para cada modelo com um conversor personalizado ou hierarquia polimórfica. - Para caminhos quentes, execute uma comparação rápida com
BenchmarkDotNete confirme que os números de throughput e alocações se moveram na direção certa em vez de regredir por causa de um acidentalnew JsonSerializerOptions()por chamada (sempre faça cache e reutilize a instância de options; construí-la a cada chamada é a regressão de desempenho mais comum nesta migração).
Plano de rollback
Esta migração é reversível por projeto mas não trivialmente, porque o passo 1 muda seu formato de saída. A estratégia limpa é uma abordagem strangler: migre um assembly ou um endpoint por vez, mantenha o Newtonsoft.Json referenciado até o último consumidor ter sido movido, e proteja os endpoints arriscados atrás de um feature flag que possa rotear de volta para o formatador do Newtonsoft.Json. Uma vez que você tenha apagado o PackageReference e enviado o novo formato de saída para os consumidores externos, fazer rollback significa readicionar o pacote e reverter a mudança de formato em todos os lugares de uma vez, o que é uma versão coordenada, não um git revert. Não apague a referência ao pacote até que as diferenças de amostras de referência tenham ficado verdes na telemetria de produção por pelo menos um ciclo de versão.
Problemas que encontramos
- Perda silenciosa de dados por diferenciação de maiúsculas. Um objeto de configuração desserializado de um arquivo com chaves
PascalCasevoltou com cada propriedade no seu valor padrão porque oSystem.Text.Jsoncorrespondeu diferenciando maiúsculas contra os membros em camelCase. Nada lançou exceção. A solução foiPropertyNameCaseInsensitive = true, e a lição foi verificar valores, não apenas se “analisou”. - Os ida e volta de
DateTimese desviaram. ODateTimeZoneHandlingdoNewtonsoft.Jsonvinha normalizando timestamps silenciosamente.System.Text.Jsonlê o formato de ida e volta ISO 8601 e preserva o offset, então os timestamps armazenados voltaram com um kind diferente. O conversor personalizado do passo 3 mais a correção de o valor JSON não pôde ser convertido para System.DateTime resolveu isso. - Ciclos de objetos lançavam exceções em vez de serem descartados.
ReferenceLoopHandling.Ignorevinha mascarando uma referência circular genuína em uma propriedade de navegação do EF Core.System.Text.Jsona trouxe à tona como um possível ciclo de objetos foi detectado.ReferenceHandler.IgnoreCyclesé a ponte, mas a melhor solução foi um DTO de projeção que não tinha o loop em absoluto. - Um
new JsonSerializerOptions()por requisição afundou o throughput. Construir o objeto de options dentro de um handler quente derrota o cache de metadados interno e foi mais lento que o código doNewtonsoft.Jsonque substituiu. Faça cache de umstatic readonly JsonSerializerOptionse reutilize-o.
Comments
Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.