1. Introducción
En esta clase haremos el paso desde "solo déjame guardar la lista" hacia una serialización flexible, precisa y eficiente usando System.Text.Json. En proyectos .NET modernos JSON es el estándar de facto para el intercambio de datos. Las llamadas básicas Serialize/Deserialize son sencillas, pero las tareas reales requieren ajustes finos: ignorar/renombrar campos, gestionar formatos de fecha, protección contra ciclos, trabajar con grandes volúmenes de datos, converters personalizados, etc.
Veremos no solo los métodos de JsonSerializer, sino también las opciones mediante JsonSerializerOptions, atributos, trabajo con Stream, gestión de memoria e inyección de tus reglas de serialización mediante JsonConverter.
Breve historia y posicionamiento de System.Text.Json
Durante mucho tiempo en .NET dominó Newtonsoft.Json (Json.NET) — flexible y maduro, pero no siempre el más rápido ni el más ligero en dependencias. Con .NET Core 3.0 apareció el integrado System.Text.Json: alto rendimiento, mínimas dependencias (parte de la plataforma), integración cercana con ASP.NET Core y evolución continua con los lanzamientos de .NET.
2. Clases y métodos principales
El elemento básico es la clase estática JsonSerializer, que ofrece dos direcciones:
- Serialización: objeto → cadena JSON (Serialize)
- Deserialización: cadena JSON → objeto del tipo necesario (Deserialize)
Ejemplo de serialización de un objeto simple
using System.Text.Json;
var person = new Person { Name = "Iván", Age = 30 };
string jsonString = JsonSerializer.Serialize(person);
Console.WriteLine(jsonString); // {"Name":"Iván","Age":30}
Ejemplo de deserialización
var json = "{\"Name\":\"Ana\",\"Age\":22}";
var anna = JsonSerializer.Deserialize<Person>(json);
Console.WriteLine(anna.Name); // Ana
Nota: el tipo Person ya está implementado en lecciones previas — lo usamos también aquí.
3. Control de la serialización: JsonSerializerOptions
En proyectos reales casi siempre necesitas configuraciones: nombres de propiedades en camelCase, formatos de fecha, manejo de ciclos, valores por defecto, etc. Todo eso se controla mediante JsonSerializerOptions.
Ejemplo de configuración
var options = new JsonSerializerOptions
{
WriteIndented = true, // Formatear JSON bonito (añade espacios y saltos de línea)
PropertyNameCaseInsensitive = true, // Ignorar mayúsculas/minúsculas de nombres de propiedades al deserializar
PropertyNamingPolicy = JsonNamingPolicy.CamelCase // camelCase para propiedades (en lugar de PascalCase)
};
string json = JsonSerializer.Serialize(person, options);
/*
{
"name": "Iván",
"age": 30
}
*/
¿Por qué es importante? La mayoría de frameworks frontend esperan camelCase, no el PascalCase típico de .NET.
4. Atributos: System.Text.Json.Serialization
A veces es más cómodo controlar la serialización directamente desde el modelo usando atributos. Se añaden a campos/propiedades para influir en nombres, inclusión/exclusión y tratamiento de valores.
Atributos principales
| Atributo | Qué hace |
|---|---|
|
Excluye la propiedad de la serialización/deserialización |
|
Usa otro nombre en el JSON |
|
Incluye una propiedad/campo no público en la serialización |
|
Controla el manejo de valores numéricos |
Ejemplo: control de propiedades mediante atributos
using System.Text.Json.Serialization;
public class Person
{
[JsonPropertyName("full_name")]
public string Name { get; set; }
[JsonIgnore]
public int SecretCode { get; set; }
public int Age { get; set; }
}
var person = new Person { Name = "Petr", Age = 45, SecretCode = 123 };
string json = JsonSerializer.Serialize(person);
// {"full_name":"Petr","Age":45}
Observa: SecretCode no apareció en el JSON, y Name se serializó como "full_name".
5. Serialización de colecciones y objetos anidados
Las colecciones son sencillas
var numbers = new List<int> { 1, 2, 3 };
string json = JsonSerializer.Serialize(numbers); // [1,2,3]
var people = new List<Person> {
new Person { Name = "Ana", Age = 20 },
new Person { Name = "Máximo", Age = 40 }
};
string jsonList = JsonSerializer.Serialize(people);
// [{"Name":"Ana","Age":20},{"Name":"Máximo","Age":40}]
Estructuras anidadas
public class Group
{
public string Name { get; set; }
public List<Person> Members { get; set; }
}
var group = new Group
{
Name = "Desarrolladores",
Members = new List<Person>
{
new Person { Name = "Sasha", Age = 23 },
new Person { Name = "Masha", Age = 28 }
}
};
string jsonGroup = JsonSerializer.Serialize(group, options);
/*
{
"name": "Desarrolladores",
"members": [
{ "name": "Sasha", "age": 23 },
{ "name": "Masha", "age": 28 }
]
}
*/
6. Deserialización: ¿qué es importante saber?
var json = "[{\"Name\":\"Iván\",\"Age\":21}]";
var list = JsonSerializer.Deserialize<List<Person>>(json);
Console.WriteLine(list[0].Name); // Iván
Escenario frecuente: si en el JSON falta algún campo, la propiedad correspondiente obtendrá el valor por defecto. Los campos extra en el JSON que no existen en el modelo son ignorados. Pero si los tipos no coinciden (por ejemplo, llegó una cadena en lugar de un número) — en la deserialización se lanzará una excepción.
7. Manejo de fechas, tiempos, formatos y valores numéricos
public class Meeting
{
public string Topic { get; set; }
public DateTime Time { get; set; }
}
var meeting = new Meeting { Topic = "Reunión", Time = DateTime.Now };
string json = JsonSerializer.Serialize(meeting);
// {"Topic":"Reunión","Time":"2024-06-06T20:30:00.0000000+03:00"}
Por defecto DateTime se serializa en ISO 8601. ¿Necesitas otro formato (por ejemplo, solo la fecha)? Usa una propiedad separada o un converter personalizado (ver más abajo).
FAQ: Para que los números se serialicen como cadenas (por ejemplo, teléfonos o IDs grandes), usa el atributo [JsonNumberHandling(JsonNumberHandling.WriteAsString)].
8. Streams y trabajo con archivos
Puedes trabajar no solo con cadenas, sino también con Stream — esto es importante para datos grandes (archivos, red).
Ejemplo de escritura a archivo
using var fs = File.Create("person.json");
JsonSerializer.Serialize(fs, person);
// ¡No olvides llamar fs.Flush() o usar using!
Ejemplo de lectura desde archivo
using var fs = File.OpenRead("person.json");
var restored = JsonSerializer.Deserialize<Person>(fs);
Con streams están disponibles métodos asincrónicos SerializeAsync/DeserializeAsync — útiles para servicios de alta carga.
9. Converters personalizados
Si las reglas estándar no encajan (formatos de fecha/número no estándar, valores complejos, estructuras propias) — escribes un JsonConverter.
Ejemplo: fecha solo como "dd.MM.yyyy"
public class CustomDateConverter : JsonConverter<DateTime>
{
public override DateTime Read(
ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return DateTime.ParseExact(reader.GetString(), "dd.MM.yyyy", null);
}
public override void Write(
Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString("dd.MM.yyyy"));
}
}
var options = new JsonSerializerOptions();
options.Converters.Add(new CustomDateConverter());
var dt = new DateTime(2024, 6, 1);
string json = JsonSerializer.Serialize(dt, options); // "01.06.2024"
Los converters personalizados son útiles para serializar coordenadas, vectores, colores, fechas y monedas no estándar, varios formatos de ID, etc.
10. Matices útiles
Manejo de referencias cíclicas y jerarquías profundas
var options = new JsonSerializerOptions
{
ReferenceHandler = ReferenceHandler.Preserve, // Preserva todos los objetos con $id/$ref
WriteIndented = true
};
Importante: en el JSON aparecerán propiedades internas $id y $ref. Para intercambio con sistemas externos que no las entienden, esto puede no encajar.
Diferencias entre System.Text.Json y Newtonsoft.Json
System.Text.Json ya es muy potente, pero aún no cubre todos los escenarios de Newtonsoft.Json (constructores privados, objetos dinámicos complejos, etc.). Para la mayoría de tareas estándar recomendamos el serializador integrado — es más rápido y evita dependencias innecesarias.
Trabajo interactivo con JSON: API DOM
Cuando necesitas "recorrer" un JSON sin un modelo completo, usa JsonDocument y JsonElement.
using var doc = JsonDocument.Parse(jsonString);
JsonElement root = doc.RootElement;
if (root.TryGetProperty("Name", out var nameProperty))
{
Console.WriteLine(nameProperty.GetString());
}
11. Opciones útiles y sus efectos
| Propiedad | Valor/Propósito |
|---|---|
|
true — formateo con sangrías |
|
true — ignorar mayúsculas/minúsculas de nombres de propiedades al deserializar |
|
|
|
Reglas para ignorar null/valores por defecto |
|
, |
|
true — permitir coma al final de un array |
|
Convertir números a cadenas/viceversa (y otros) |
|
Lista de converters personalizados |
12. Errores comunes y consejos prácticos
Error №1: tipo incorrecto al deserializar. Si serializaste una lista, deserializa exactamente a una lista: List<T>, no a un objeto único.
Error №2: mayúsculas/minúsculas incorrectas en nombres de propiedades. Sin configurar la insensibilidad de mayúsculas, las propiedades pueden "no encontrarse". Usa PropertyNameCaseInsensitive o configura PropertyNamingPolicy.
Error №3: manejo incorrecto de fechas. Por defecto el formato es ISO 8601. Si necesitas otro — crea y aplica un converter (JsonConverter<DateTime>).
Error №4: esperar serialización de campos privados/estáticos. Por defecto se toman propiedades públicas. Para casos no estándar usa atributos correspondientes (por ejemplo, [JsonInclude]).
Error №5: malinterpretar valores por defecto. Campo ausente en JSON → valor por defecto para la propiedad. Tenlo en cuenta en la lógica.
Error №6: mala gestión de streams. Cierra recursos con using o await using para evitar fugas y bloqueos.
GO TO FULL VERSION