1. Introducción
Durante mucho tiempo en el ecosistema .NET la herramienta principal para trabajar con JSON fue el estupendo paquete externo Newtonsoft.Json (también conocido como Json.NET). Es potente, flexible y sigue usándose mucho. Pero con la llegada de nuevas versiones .NET 9 y C# 14, Microsoft decidió que ya tocaba tener su propio serializador JSON integrado y de alto rendimiento. Así apareció System.Text.Json.
¿Por qué un enfoque nuevo? System.Text.Json está diseñado pensando en las realidades modernas y soluciona problemas acumulados tras años de uso de bibliotecas externas. Está optimizado para máxima velocidad y seguridad, encaja genial en escenarios async y Web API, y —lo mejor— no requiere instalación vía NuGet: ya viene con la plataforma.
Claro, Newtonsoft.Json no ha desaparecido, y lo veremos más adelante. Pero para la mayoría de proyectos nuevos System.Text.Json es la elección por defecto. Prepárate: ahora vamos a enseñar a nuestros objetos a hablar JSON.
2. Fundamentos de System.Text.Json
Serialización simple de un objeto
Bueno, empecemos con la magia básica. Serialicemos nuestro objeto a una cadena JSON.
using System;
using System.Text.Json; // ¡No olvides añadirlo!
public class Player
{
public string Name { get; set; }
public int Health { get; set; }
public bool IsAlive { get; set; }
}
// En alguna parte de tu programa:
Player player = new Player { Name = "Aragorn", Health = 100, IsAlive = true };
// Serialización:
string json = JsonSerializer.Serialize(player);
Console.WriteLine(json); // Mostrará: {"Name":"Aragorn","Health":100,"IsAlive":true}
Comentario: Si recién empiezas con la serialización, este ejemplo muestra lo sencillo que es: JsonSerializer.Serialize — y listo.
Deserialización de un objeto
Resucitemos un objeto desde una cadena JSON:
string incomingJson = "{\"Name\":\"Legolas\",\"Health\":88,\"IsAlive\":true}";
Player player2 = JsonSerializer.Deserialize<Player>(incomingJson);
Console.WriteLine(player2.Name); // Legolas
Console.WriteLine(player2.Health); // 88
Console.WriteLine(player2.IsAlive); // true
Comentario: Si la estructura del JSON coincide con tu clase —todo funciona como un reloj. Si no —pueden aparecer excepciones o valores por defecto.
3. Principios y mecánica de la serialización
Cómo ocurre el mapeo
System.Text.Json por defecto usa los mismos nombres de propiedad que en la clase. ¡El case importa! Si en el JSON está health en lugar de Health, la deserialización no funcionará —la propiedad quedará con el valor por defecto (0, false o null).
Por ejemplo:
// JSON con claves en minúsculas:
string badJson = "{\"name\":\"Gimli\",\"health\":120,\"isAlive\":true}";
Player player3 = JsonSerializer.Deserialize<Player>(badJson);
Console.WriteLine(player3.Name); // vacío
Console.WriteLine(player3.Health); // 0
Console.WriteLine(player3.IsAlive); // false
Dato curioso: Muchos API usan claves en camelCase (health), mientras que en C# es habitual PascalCase (Health). Esto se soluciona con configuraciones (más abajo).
4. Controlando la serialización — opciones y configuraciones
Formateo JSON: salida "legible"
A veces queremos JSON bonito, no compacto —para configs o logs.
var options = new JsonSerializerOptions
{
WriteIndented = true // Añadir indentación
};
string prettyJson = JsonSerializer.Serialize(player, options);
Console.WriteLine(prettyJson);
/*
{
"Name": "Frodo",
"Health": 50,
"IsAlive": true,
"Inventory": [
"Ring",
"Bread",
"Torch"
],
"Position": {
"X": 5,
"Y": 15
}
}
*/
Se usa la propiedad WriteIndented, y el serializador añade saltos de línea y espacios.
Control del estilo de nombres (CamelCase vs PascalCase)
Si trabajas con un Web API donde todas las claves están en camelCase, activa la política de nombres:
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
string camelCaseJson = JsonSerializer.Serialize(player, options);
// {"name":"Frodo","health":50,"isAlive":true,"inventory":["Ring","Bread","Torch"],"position":{"x":5,"y":15}}
Así, en la deserialización esas claves se mapearán correctamente:
string apiJson = "{\"name\":\"Bilbo\",\"health\":40,\"isAlive\":true,\"inventory\":[\"Mug\"],\"position\":{\"x\":10,\"y\":5}}";
Player bilbo = JsonSerializer.Deserialize<Player>(apiJson, options);
Console.WriteLine(bilbo.Name); // Bilbo
5. Matices útiles
Usando el atributo [JsonIgnore]
A veces no quieres serializar todas las propiedades —por ejemplo datos privados o valores calculados temporales.
using System.Text.Json.Serialization;
public class Player
{
public string Name { get; set; }
public int Health { get; set; }
[JsonIgnore] // Esta propiedad no irá al JSON
public bool IsSecretCharacter { get; set; }
}
Ahora al serializar:
var player = new Player { Name = "Boromir", Health = 80, IsSecretCharacter = true };
string json = JsonSerializer.Serialize(player);
Console.WriteLine(json); // {"Name":"Boromir","Health":80}
Al deserializar, IsSecretCharacter obtendrá el valor por defecto (false).
Usando [JsonPropertyName("...")]
Imagina que en tu código la propiedad se llama IsAlive, pero en el JSON debe ser "status":
using System.Text.Json.Serialization;
public class Player
{
public string Name { get; set; }
public int Health { get; set; }
[JsonPropertyName("status")]
public bool IsAlive { get; set; }
}
La serialización quedará así:
var player = new Player { Name = "Pippin", Health = 60, IsAlive = false };
string json = JsonSerializer.Serialize(player);
Console.WriteLine(json); // {"Name":"Pippin","Health":60,"status":false}
Y al deserializar, la clave "status" rellenará correctamente la propiedad IsAlive.
Limitaciones y consideraciones de seguridad integradas
- Por defecto se serializan solo las propiedades públicas con getters/setters; campos/propiedades privadas se ignoran.
- En enlaces cíclicos se lanza excepción: "A possible object cycle was detected".
- A diferencia de Newtonsoft.Json, el serializador estándar depende menos de trucos "mágicos" con tipos —lo que lo hace más seguro y rápido para escenarios comunes.
6. Errores comunes y trampas
Accidentalmente cambias el nombre de una propiedad en el JSON y olvidas actualizar el código —como resultado la propiedad obtiene el valor por defecto (null, 0, false).
Falta un campo necesario en el JSON —la propiedad correspondiente del objeto quedará por defecto (ver la documentación).
La propiedad no tiene setter público —no se llenará en la deserialización.
Cambiaste la estructura de clases anidadas o colecciones —la deserialización puede romperse o producir resultados inesperados.
Nombres iguales en distintos niveles de anidamiento (en el padre y en el hijo) confunden y dificultan el debug.
A veces la causa es la versión de la plataforma: versiones antiguas de System.Text.Json tenían peores resultados con ciertos tipos (por ejemplo, Dictionary, DateTime, enum), pero en .NET 7/8/9 mucho está corregido.
GO TO FULL VERSION