Start Debugging

Migrar de Newtonsoft.Json 13 a System.Text.Json en una base de código grande de .NET 11

Una guía con versiones fijadas para reemplazar Newtonsoft.Json 13.0.4 por el System.Text.Json integrado en .NET 11: los mapeos de atributos y opciones, los valores predeterminados que cambian en silencio tu formato de salida, una estrategia de despliegue por etapas, la verificación y los problemas que afectan a las bases de código grandes.

Reemplazar Newtonsoft.Json por System.Text.Json en una base de código grande rara vez es un trabajo de buscar y reemplazar. Las dos bibliotecas difieren en sus valores predeterminados de maneras que cambian tu salida serializada y rompen la deserialización en silencio, así que un reemplazo ingenuo envía un cambio de contrato a cada consumidor de tu JSON. Reserva unos días para un servicio pequeño y de dos a cuatro semanas para una base de código extensa con convertidores personalizados, payloads polimórficos y análisis con dynamic/JObject. La ganancia es real: System.Text.Json se incluye de fábrica con el runtime, serializa aproximadamente el doble de rápido con una fracción de las asignaciones, y es el único de los dos que se ejecuta bajo Native AOT. Este artículo fija Newtonsoft.Json 13.0.4 (la versión estable actual, lanzada el 2025-12-30) como origen y el System.Text.Json integrado en el .NET 11 SDK con C# 14 como destino. Si todavía estás decidiendo si moverte o no, lee primero System.Text.Json vs Newtonsoft.Json en 2026; este artículo asume que ya decidiste migrar.

Por qué migrar ahora

Qué se rompe

El peligro de esta migración no es el código que no compila. Es el código que compila bien y cambia tu formato de salida. Esta tabla es la que hay que leer dos veces.

ÁreaCambioSeveridad
Coincidencia de nombres de propiedadNewtonsoft.Json no distingue mayúsculas al leer por defecto; System.Text.Json sí distinguealta
Comentarios y comas finalesAceptados por defecto en Newtonsoft.Json, lanzan JsonException en System.Text.Jsonalta
JSON con comillas simples / sin comillasAceptado por Newtonsoft.Json, rechazado por diseño en System.Text.Jsonalta
Valor no-string en propiedad stringNewtonsoft.Json convierte 1 o true; System.Text.Json lanza una excepciónalta
Números entre comillasNewtonsoft.Json lee "23" en un int; System.Text.Json necesita NumberHandlingmedia
Escape de caracteresSystem.Text.Json escapa de forma más agresiva, así que los bytes de salida difieren para no-ASCII y HTMLmedia
[JsonProperty("name")]Se convierte en [JsonPropertyName("name")]; no hay opciones combinadas de ignore/required en un atributomedia
TypeNameHandling.AllNo hay equivalente, por diseño. El polimorfismo usa [JsonDerivedType] en su lugaralta
JObject / JToken / dynamicReemplazados por JsonNode / JsonDocument / JsonElement con una API diferentemedia
JsonConvert.PopulateObjectNo hay equivalente integrado; necesita un convertidor personalizado o una fusión manualmedia
ReferenceLoopHandling.IgnoreNo hay modo “descartar el bucle en silencio”; obtienes ReferenceHandler.Preserve o rediseñas el grafomedia
DateFormatString, DateTimeZoneHandlingNo hay opción global de formato de fecha; necesita un JsonConverter<DateTime> personalizadomedia
Precedencia de registro de convertidoresLa colección Converters ahora anula un atributo a nivel de tipo (invertido respecto a Newtonsoft.Json)baja

La referencia autoritativa para cada fila aquí es la guía de migración de Microsoft, que también lista el puñado de características (consultas JsonPath, TypeNameHandling.All, análisis de comillas simples) que no tienen solución alternativa.

Lista de comprobación previa

Haz todo esto antes de borrar un solo using Newtonsoft.Json.

  1. Fija el contrato. Si tu JSON cruza un límite de proceso (una API pública, una cola de mensajes, una columna persistida), captura muestras de referencia de la salida actual. Serializa un conjunto representativo de objetos con Newtonsoft.Json 13.0.4 y guarda las cadenas como fixtures de prueba. Estas son tu oráculo de regresión.
  2. Inventaría la superficie. Busca con grep todo lo que toque la biblioteca antigua para conocer el tamaño del trabajo:
    # 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" .
    Verifica: la lista de archivos coincide con tu modelo mental de dónde vive la serialización. Las sorpresas aquí (un convertidor enterrado en un ayudante de registro) son exactamente lo que quieres encontrar ahora.
  3. Marca las características sin solución alternativa. Busca específicamente TypeNameHandling, SelectToken, SelectTokens y fixtures de prueba con comillas simples. Si encuentras TypeNameHandling.All o .Auto, detente y diseña el reemplazo del polimorfismo antes de continuar, porque no hay un sustituto directo para ello.
  4. Confirma el destino. Ejecuta dotnet --version y confirma 11.0.x. System.Text.Json viene integrado, así que no hay paquete que agregar para los escenarios principales; solo agregas System.Text.Json como un PackageReference explícito si necesitas una versión out-of-band más nueva que la que incluye el SDK.

Pasos de migración

  1. Mapea las opciones globales.

    Newtonsoft.Json centraliza el comportamiento en JsonSerializerSettings. System.Text.Json usa JsonSerializerOptions. Traduce tu objeto de configuración existente campo por campo; no aceptes los valores predeterminados de System.Text.Json a ciegas, porque difieren de lo que tu código ha estado emitiendo durante años.

    // .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
    };

    Verifica: serializa tus objetos de muestra de referencia con estas options y compáralos con los fixtures del paso 1 del preflight. La diferencia debería estar vacía o explicada. PropertyNameCaseInsensitive = true es la línea más importante para las bases de código grandes, porque innumerables rutas de deserialización dependen silenciosamente de la coincidencia sin distinción de mayúsculas de Newtonsoft.Json.

  2. Reemplaza los atributos.

    El renombrado de atributos es mecánico, pero [JsonProperty] empaquetaba varias responsabilidades en un solo atributo que System.Text.Json separa.

    // .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; } = "";
    }

    Verifica: el proyecto compila con cero directivas using de Newtonsoft.Json en el ensamblado de modelos, y una prueba de ida y vuelta (Deserialize(Serialize(order))) preserva cada campo.

  3. Porta los convertidores personalizados.

    Aquí es donde se van las horas. Las formas son similares pero los contratos difieren: los convertidores de System.Text.Json trabajan sobre Utf8JsonReader (un ref struct) y Utf8JsonWriter, y Read se llama posicionado en el primer 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));
    }

    Regístralo en options.Converters, no solo como un atributo de tipo, y nota el cambio de precedencia: en System.Text.Json un convertidor en la colección Converters anula un atributo [JsonConverter] a nivel de tipo, lo contrario de Newtonsoft.Json. Los detalles mecánicos están en cómo escribir un JsonConverter personalizado en System.Text.Json. Verifica cada convertidor portado contra su propio fixture, no solo el payload de extremo a extremo, para que una sorpresa de precedencia no se esconda detrás de una prueba de integración que pasa.

  4. Reemplaza el polimorfismo y TypeNameHandling.

    Si usabas TypeNameHandling para hacer un ida y vuelta de una jerarquía de clases, no hay equivalente, y eso es deliberado: TypeNameHandling.All es un vector de ejecución remota de código bien conocido. System.Text.Json hace polimorfismo discriminado con atributos en el 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; } }

    Esto emite un discriminador "$type": "dog" y lo lee de vuelta al subtipo correcto. Verifica: serializa una List<Animal> de subtipos mezclados, deserialízala y comprueba que los tipos en tiempo de ejecución sobreviven. Nota que el formato de salida cambió (una cadena discriminadora explícita en lugar del $type calificado por ensamblado de Newtonsoft.Json), así que cualquier consumidor externo debe actualizarse al mismo tiempo.

  5. Convierte el análisis con dynamic y JObject.

    El código que hurga en JSON sin tipo vía JObject/JToken/dynamic pasa a JsonNode (mutable) o JsonDocument/JsonElement (de solo lectura, agrupado en un 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>();

    La única trampa: JsonDocument posee un búfer agrupado en un pool y es IDisposable, a diferencia de JObject. Envuélvelo en un using o filtrarás el búfer alquilado. Prefiere JsonNode cuando necesites un árbol mutable parecido a JObject. Verifica: cada antigua ruta de acceso a JObject tiene una prueba unitaria que ejercita las mismas búsquedas de clave.

  6. Cambia la integración de ASP.NET Core.

    Si la base de código llama a AddNewtonsoftJson() en Program.cs, eliminarlo conmuta todo el pipeline a System.Text.Json. Los valores predeterminados web de ASP.NET Core ya habilitan camelCase, la coincidencia sin distinción de mayúsculas y la lectura de números entre comillas, así que muchas de tus opciones manuales se vuelven redundantes en la ruta 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
    });

    Atención al límite de profundidad: ASP.NET Core limita MaxDepth de System.Text.Json a 32, no al valor predeterminado de la biblioteca de 64. Los payloads profundamente anidados que funcionaban con AddNewtonsoftJson() pueden empezar a lanzar excepciones. Verifica: ejecuta las pruebas de integración del controlador y confirma que ningún payload supera el límite de profundidad.

Verificación

Ejecuta esta lista de pruebas de humo después de cada PR, no solo al final:

Plan de reversión

Esta migración es reversible por proyecto pero no trivialmente, porque el paso 1 cambia tu formato de salida. La estrategia limpia es un enfoque strangler: migra un ensamblado o un endpoint a la vez, mantén Newtonsoft.Json referenciado hasta que el último consumidor se haya movido, y protege los endpoints riesgosos detrás de un feature flag que pueda enrutar de vuelta al formateador de Newtonsoft.Json. Una vez que hayas borrado el PackageReference y enviado el nuevo formato de salida a los consumidores externos, revertir significa volver a agregar el paquete y deshacer el cambio de formato en todas partes a la vez, lo cual es una versión coordinada, no un git revert. No borres la referencia al paquete hasta que las diferencias de muestras de referencia hayan estado en verde en la telemetría de producción durante al menos un ciclo de versión.

Problemas que encontramos

Fuentes

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< Volver