Trabajando con archivos JSON
JSON (JavaScript Object Notation) se ha consolidado como uno de los formatos de intercambio de datos más populares en el desarrollo moderno. Su sintaxis simple, legible y liviana lo convierte en la elección preferida para APIs web, archivos de configuración y almacenamiento de datos estructurados. En C#, trabajar con JSON es una tarea fundamental que nos permite intercambiar información con servicios web, almacenar configuraciones de aplicación y persistir datos de forma estructurada.
El manejo eficiente de JSON nos permite crear aplicaciones que se comunican seamlessly con otros sistemas y servicios. A través de las librerías nativas de .NET, podemos serializar objetos C# a formato JSON y deserializar datos JSON de vuelta a objetos tipados, manteniendo la integridad de los tipos y la facilidad de uso que caracteriza al lenguaje.
En este artículo exploraremos las técnicas esenciales para trabajar con archivos JSON en C#, desde operaciones básicas de lectura y escritura hasta el manejo de estructuras complejas y configuraciones avanzadas de serialización.
Qué es JSON y su estructura
JSON (JavaScript Object Notation) es un formato de intercambio de datos basado en texto que utiliza una sintaxis derivada de JavaScript, aunque es independiente del lenguaje. Su popularidad se debe a su simplicidad, legibilidad y soporte universal.
Elementos básicos de JSON
Tipo de dato | Sintaxis JSON | Ejemplo |
---|---|---|
Objeto | { "clave": "valor" } |
{ "nombre": "Juan", "edad": 30 } |
Array | [ elemento1, elemento2 ] |
[ "rojo", "verde", "azul" ] |
Cadena | "texto" |
"Hola mundo" |
Número | 123 o 45.67 |
42 , 3.14159 |
Booleano | true o false |
true |
Nulo | null |
null |
Estructura típica de un archivo JSON
{
"usuario": {
"id": 1,
"nombre": "Ana García",
"email": "ana.garcia@email.com",
"activo": true,
"roles": ["usuario", "editor"],
"configuracion": {
"tema": "oscuro",
"idioma": "es"
}
}
}
System.Text.Json en .NET
A partir de .NET Core 3.0, Microsoft introdujo System.Text.Json
como la librería nativa para trabajar con JSON. Esta librería ofrece alto rendimiento, bajo consumo de memoria y una API moderna para la serialización y deserialización de datos JSON.
Ventajas de System.Text.Json
Característica | Descripción |
---|---|
Rendimiento | Optimizada para velocidad y eficiencia de memoria |
Seguridad | Protecciones integradas contra ataques de deserialización |
Compatibilidad | Funciona seamlessly con el ecosistema .NET |
Configurabilidad | Opciones extensas para personalizar el comportamiento |
Instalación y configuración
Para .NET 8, System.Text.Json
viene incluido por defecto. Solo necesitas agregar la directiva using:
using System.Text.Json;
using System.Text.Json.Serialization;
Serialización de objetos a JSON
La serialización es el proceso de convertir objetos C# en formato JSON. Esto es útil para almacenar datos, enviar información a APIs o crear archivos de configuración.
Serialización básica
using System;
using System.Text.Json;
using System.IO;
// Definimos una clase para nuestros datos
public class Persona
{
public string Nombre { get; set; }
public int Edad { get; set; }
public string Email { get; set; }
public bool Activo { get; set; }
public string[] Hobbies { get; set; }
}
class Program
{
static void Main()
{
// Creamos un objeto de ejemplo
var persona = new Persona
{
Nombre = "Carlos Ruiz",
Edad = 28,
Email = "carlos.ruiz@email.com",
Activo = true,
Hobbies = new[] { "lectura", "programacion", "futbol" }
};
// Serializamos el objeto a JSON
string jsonString = JsonSerializer.Serialize(persona);
Console.WriteLine("JSON generado:");
Console.WriteLine(jsonString);
// Guardamos el JSON en un archivo
File.WriteAllText("persona.json", jsonString);
Console.WriteLine("Archivo persona.json creado exitosamente");
}
}
Configuración de opciones de serialización
Para controlar cómo se serializa el JSON, podemos usar JsonSerializerOptions
:
using System;
using System.Text.Json;
using System.IO;
class Program
{
static void Main()
{
var persona = new Persona
{
Nombre = "María López",
Edad = 35,
Email = "maria.lopez@email.com",
Activo = true,
Hobbies = new[] { "yoga", "cocina", "fotografía" }
};
// Configuramos las opciones de serialización
var opciones = new JsonSerializerOptions
{
WriteIndented = true, // JSON formateado con indentación
PropertyNamingPolicy = JsonNamingPolicy.CamelCase, // Propiedades en camelCase
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull // Ignorar valores null
};
// Serializamos con las opciones configuradas
string jsonFormatado = JsonSerializer.Serialize(persona, opciones);
Console.WriteLine("JSON formateado:");
Console.WriteLine(jsonFormatado);
// Guardamos en archivo
File.WriteAllText("persona_formateada.json", jsonFormatado);
}
}
Serialización de colecciones
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.IO;
class Program
{
static void Main()
{
// Creamos una lista de personas
var personas = new List<Persona>
{
new Persona { Nombre = "Juan Pérez", Edad = 30, Email = "juan@email.com", Activo = true },
new Persona { Nombre = "Ana Martín", Edad = 25, Email = "ana@email.com", Activo = false },
new Persona { Nombre = "Pedro Sánchez", Edad = 40, Email = "pedro@email.com", Activo = true }
};
// Serializamos la colección
var opciones = new JsonSerializerOptions { WriteIndented = true };
string jsonPersonas = JsonSerializer.Serialize(personas, opciones);
Console.WriteLine("Lista de personas en JSON:");
Console.WriteLine(jsonPersonas);
// Guardamos la colección en un archivo
File.WriteAllText("personas.json", jsonPersonas);
Console.WriteLine("Archivo personas.json creado");
}
}
Deserialización de JSON a objetos
La deserialización es el proceso inverso: convertir datos JSON en objetos C# tipados. Esto nos permite leer archivos JSON, procesar respuestas de APIs y cargar configuraciones.
Deserialización básica
using System;
using System.Text.Json;
using System.IO;
class Program
{
static void Main()
{
try
{
// Leemos el archivo JSON
string jsonContent = File.ReadAllText("persona.json");
Console.WriteLine("Contenido del archivo:");
Console.WriteLine(jsonContent);
// Deserializamos el JSON a un objeto Persona
Persona personaDeserializada = JsonSerializer.Deserialize<Persona>(jsonContent);
// Mostramos los datos deserializados
Console.WriteLine("\nDatos deserializados:");
Console.WriteLine($"Nombre: {personaDeserializada.Nombre}");
Console.WriteLine($"Edad: {personaDeserializada.Edad}");
Console.WriteLine($"Email: {personaDeserializada.Email}");
Console.WriteLine($"Activo: {personaDeserializada.Activo}");
Console.WriteLine($"Hobbies: {string.Join(", ", personaDeserializada.Hobbies)}");
}
catch (FileNotFoundException)
{
Console.WriteLine("El archivo persona.json no existe. Ejecuta primero el ejemplo de serialización.");
}
catch (JsonException ex)
{
Console.WriteLine($"Error al deserializar JSON: {ex.Message}");
}
}
}
Deserialización con opciones configuradas
using System;
using System.Text.Json;
using System.IO;
class Program
{
static void Main()
{
try
{
string jsonContent = File.ReadAllText("persona_formateada.json");
// Configuramos las opciones para la deserialización
var opciones = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
PropertyNameCaseInsensitive = true // Permite coincidencias case-insensitive
};
// Deserializamos con las opciones
Persona persona = JsonSerializer.Deserialize<Persona>(jsonContent, opciones);
Console.WriteLine("Deserialización exitosa:");
Console.WriteLine($"Nombre: {persona.Nombre}");
Console.WriteLine($"Edad: {persona.Edad}");
Console.WriteLine($"Email: {persona.Email}");
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
}
}
Deserialización de colecciones
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.IO;
class Program
{
static void Main()
{
try
{
// Leemos el archivo de la colección
string jsonContent = File.ReadAllText("personas.json");
// Deserializamos a una lista de personas
List<Persona> personas = JsonSerializer.Deserialize<List<Persona>>(jsonContent);
Console.WriteLine($"Se deserializaron {personas.Count} personas:");
foreach (var persona in personas)
{
Console.WriteLine($"- {persona.Nombre}, {persona.Edad} años, Estado: {(persona.Activo ? "Activo" : "Inactivo")}");
}
// Filtrar y mostrar solo las personas activas
var personasActivas = personas.FindAll(p => p.Activo);
Console.WriteLine($"\nPersonas activas: {personasActivas.Count}");
}
catch (Exception ex)
{
Console.WriteLine($"Error al procesar el archivo: {ex.Message}");
}
}
}
Personalización con atributos
Los atributos de System.Text.Json.Serialization
nos permiten controlar con precisión cómo se serializa y deserializa cada propiedad.
Atributos comunes de serialización
Atributo | Propósito | Ejemplo |
---|---|---|
[JsonPropertyName] |
Especifica el nombre de la propiedad en JSON | [JsonPropertyName("full_name")] |
[JsonIgnore] |
Excluye la propiedad de la serialización | [JsonIgnore] |
[JsonInclude] |
Incluye campos privados en la serialización | [JsonInclude] |
[JsonConverter] |
Usa un convertidor personalizado | [JsonConverter(typeof(MiConverter))] |
Ejemplo práctico con atributos
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.IO;
public class Usuario
{
[JsonPropertyName("id")]
public int Identificador { get; set; }
[JsonPropertyName("full_name")]
public string NombreCompleto { get; set; }
[JsonPropertyName("email")]
public string CorreoElectronico { get; set; }
[JsonPropertyName("birth_date")]
public DateTime FechaNacimiento { get; set; }
[JsonIgnore]
public string Contrasena { get; set; } // No se incluirá en el JSON
[JsonPropertyName("is_active")]
public bool EstaActivo { get; set; }
[JsonPropertyName("roles")]
public string[] Roles { get; set; }
// Propiedad calculada que se incluye en el JSON
[JsonPropertyName("age")]
public int Edad => DateTime.Now.Year - FechaNacimiento.Year;
}
class Program
{
static void Main()
{
var usuario = new Usuario
{
Identificador = 1,
NombreCompleto = "Elena Rodríguez",
CorreoElectronico = "elena.rodriguez@empresa.com",
FechaNacimiento = new DateTime(1990, 5, 15),
Contrasena = "supersecreta123", // Esto no aparecerá en el JSON
EstaActivo = true,
Roles = new[] { "admin", "usuario" }
};
// Serialización con nombres personalizados
var opciones = new JsonSerializerOptions { WriteIndented = true };
string json = JsonSerializer.Serialize(usuario, opciones);
Console.WriteLine("Usuario serializado con nombres personalizados:");
Console.WriteLine(json);
// Guardar en archivo
File.WriteAllText("usuario.json", json);
// Deserialización
string jsonLeido = File.ReadAllText("usuario.json");
Usuario usuarioDeserializado = JsonSerializer.Deserialize<Usuario>(jsonLeido);
Console.WriteLine($"\nDatos deserializados:");
Console.WriteLine($"ID: {usuarioDeserializado.Identificador}");
Console.WriteLine($"Nombre: {usuarioDeserializado.NombreCompleto}");
Console.WriteLine($"Email: {usuarioDeserializado.CorreoElectronico}");
Console.WriteLine($"Edad calculada: {usuarioDeserializado.Edad}");
Console.WriteLine($"Contraseña (debe estar vacía): '{usuarioDeserializado.Contrasena}'");
}
}
Manejo de estructuras JSON complejas
En aplicaciones reales, a menudo trabajamos con JSON que contiene estructuras anidadas, arrays de objetos y tipos de datos diversos.
Ejemplo de estructura compleja
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.IO;
public class Direccion
{
[JsonPropertyName("calle")]
public string Calle { get; set; }
[JsonPropertyName("numero")]
public int Numero { get; set; }
[JsonPropertyName("ciudad")]
public string Ciudad { get; set; }
[JsonPropertyName("codigo_postal")]
public string CodigoPostal { get; set; }
}
public class Contacto
{
[JsonPropertyName("tipo")]
public string Tipo { get; set; }
[JsonPropertyName("valor")]
public string Valor { get; set; }
}
public class Empleado
{
[JsonPropertyName("id")]
public int Id { get; set; }
[JsonPropertyName("nombre")]
public string Nombre { get; set; }
[JsonPropertyName("apellidos")]
public string Apellidos { get; set; }
[JsonPropertyName("fecha_contratacion")]
public DateTime FechaContratacion { get; set; }
[JsonPropertyName("salario")]
public decimal Salario { get; set; }
[JsonPropertyName("direccion")]
public Direccion Direccion { get; set; }
[JsonPropertyName("contactos")]
public List<Contacto> Contactos { get; set; }
[JsonPropertyName("habilidades")]
public string[] Habilidades { get; set; }
[JsonPropertyName("activo")]
public bool Activo { get; set; }
}
class Program
{
static void Main()
{
// Creamos un empleado con estructura compleja
var empleado = new Empleado
{
Id = 101,
Nombre = "Roberto",
Apellidos = "González Martín",
FechaContratacion = new DateTime(2020, 3, 15),
Salario = 35000.50m,
Direccion = new Direccion
{
Calle = "Gran Vía",
Numero = 25,
Ciudad = "Madrid",
CodigoPostal = "28013"
},
Contactos = new List<Contacto>
{
new Contacto { Tipo = "telefono", Valor = "+34 912 345 678" },
new Contacto { Tipo = "email", Valor = "roberto.gonzalez@empresa.com" },
new Contacto { Tipo = "linkedin", Valor = "linkedin.com/in/robertogonzalez" }
},
Habilidades = new[] { "C#", ".NET", "SQL Server", "Azure", "Git" },
Activo = true
};
// Serializar con formato indentado
var opciones = new JsonSerializerOptions
{
WriteIndented = true,
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
string json = JsonSerializer.Serialize(empleado, opciones);
Console.WriteLine("Empleado serializado:");
Console.WriteLine(json);
// Guardar en archivo
File.WriteAllText("empleado.json", json);
// Leer y deserializar
string jsonLeido = File.ReadAllText("empleado.json");
Empleado empleadoDeserializado = JsonSerializer.Deserialize<Empleado>(jsonLeido);
// Mostrar información deserializada
Console.WriteLine($"\n=== INFORMACIÓN DEL EMPLEADO ===");
Console.WriteLine($"ID: {empleadoDeserializado.Id}");
Console.WriteLine($"Nombre completo: {empleadoDeserializado.Nombre} {empleadoDeserializado.Apellidos}");
Console.WriteLine($"Fecha de contratación: {empleadoDeserializado.FechaContratacion:dd/MM/yyyy}");
Console.WriteLine($"Salario: {empleadoDeserializado.Salario:C}");
Console.WriteLine($"\nDirección:");
Console.WriteLine($" {empleadoDeserializado.Direccion.Calle} {empleadoDeserializado.Direccion.Numero}");
Console.WriteLine($" {empleadoDeserializado.Direccion.Ciudad} - {empleadoDeserializado.Direccion.CodigoPostal}");
Console.WriteLine($"\nContactos:");
foreach (var contacto in empleadoDeserializado.Contactos)
{
Console.WriteLine($" {contacto.Tipo}: {contacto.Valor}");
}
Console.WriteLine($"\nHabilidades: {string.Join(", ", empleadoDeserializado.Habilidades)}");
Console.WriteLine($"Estado: {(empleadoDeserializado.Activo ? "Activo" : "Inactivo")}");
}
}
Manejo de errores y validaciones
Es crucial implementar un manejo robusto de errores cuando trabajamos con JSON, especialmente al procesar datos de fuentes externas.
Validación y manejo de errores
using System;
using System.Text.Json;
using System.IO;
public class ConfiguracionApp
{
public string NombreApp { get; set; }
public string Version { get; set; }
public int PuertoServidor { get; set; }
public bool ModoDebug { get; set; }
public string[] ModulosHabilitados { get; set; }
}
class Program
{
static void Main()
{
// Intentamos leer y procesar diferentes archivos JSON
ProcesarArchivo("configuracion_valida.json");
ProcesarArchivo("configuracion_invalida.json");
ProcesarArchivo("archivo_inexistente.json");
}
static void ProcesarArchivo(string nombreArchivo)
{
Console.WriteLine($"\n=== Procesando {nombreArchivo} ===");
try
{
// Verificar si el archivo existe
if (!File.Exists(nombreArchivo))
{
Console.WriteLine($"Error: El archivo {nombreArchivo} no existe.");
// Crear un archivo de ejemplo si no existe
if (nombreArchivo == "configuracion_valida.json")
{
CrearArchivoEjemplo(nombreArchivo);
}
return;
}
// Leer el contenido del archivo
string contenidoJson = File.ReadAllText(nombreArchivo);
Console.WriteLine($"Contenido leído: {contenidoJson}");
// Validar que no esté vacío
if (string.IsNullOrWhiteSpace(contenidoJson))
{
Console.WriteLine("Error: El archivo está vacío.");
return;
}
// Intentar deserializar
ConfiguracionApp config = JsonSerializer.Deserialize<ConfiguracionApp>(contenidoJson);
// Validar los datos deserializados
if (ValidarConfiguracion(config))
{
MostrarConfiguracion(config);
}
else
{
Console.WriteLine("Error: La configuración no es válida.");
}
}
catch (JsonException ex)
{
Console.WriteLine($"Error de formato JSON: {ex.Message}");
Console.WriteLine("Verifica que la sintaxis del JSON sea correcta.");
}
catch (NotSupportedException ex)
{
Console.WriteLine($"Error de tipo no soportado: {ex.Message}");
}
catch (ArgumentNullException ex)
{
Console.WriteLine($"Error de argumento nulo: {ex.Message}");
}
catch (Exception ex)
{
Console.WriteLine($"Error inesperado: {ex.Message}");
}
}
static bool ValidarConfiguracion(ConfiguracionApp config)
{
if (config == null)
{
Console.WriteLine("La configuración es nula.");
return false;
}
if (string.IsNullOrEmpty(config.NombreApp))
{
Console.WriteLine("El nombre de la aplicación es requerido.");
return false;
}
if (config.PuertoServidor <= 0 || config.PuertoServidor > 65535)
{
Console.WriteLine($"Puerto inválido: {config.PuertoServidor}. Debe estar entre 1 y 65535.");
return false;
}
if (config.ModulosHabilitados == null || config.ModulosHabilitados.Length == 0)
{
Console.WriteLine("Advertencia: No hay módulos habilitados.");
}
return true;
}
static void MostrarConfiguracion(ConfiguracionApp config)
{
Console.WriteLine("✓ Configuración válida:");
Console.WriteLine($" Aplicación: {config.NombreApp}");
Console.WriteLine($" Versión: {config.Version}");
Console.WriteLine($" Puerto: {config.PuertoServidor}");
Console.WriteLine($" Modo Debug: {(config.ModoDebug ? "Activado" : "Desactivado")}");
if (config.ModulosHabilitados != null && config.ModulosHabilitados.Length > 0)
{
Console.WriteLine($" Módulos: {string.Join(", ", config.ModulosHabilitados)}");
}
}
static void CrearArchivoEjemplo(string nombreArchivo)
{
var configEjemplo = new ConfiguracionApp
{
NombreApp = "Mi Aplicación",
Version = "1.0.0",
PuertoServidor = 8080,
ModoDebug = true,
ModulosHabilitados = new[] { "autenticacion", "logging", "cache" }
};
var opciones = new JsonSerializerOptions { WriteIndented = true };
string json = JsonSerializer.Serialize(configEjemplo, opciones);
File.WriteAllText(nombreArchivo, json);
Console.WriteLine($"Se ha creado el archivo de ejemplo: {nombreArchivo}");
}
}
Consejos y mejores prácticas
Rendimiento y optimización
using System;
using System.Text.Json;
using System.IO;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
// Para archivos grandes, usar streams en lugar de cargar todo en memoria
await ProcesarArchivoGrande("datos_grandes.json");
// Reutilizar opciones de serialización
var opcionesGlobales = new JsonSerializerOptions
{
WriteIndented = false, // Más compacto para producción
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
// Usar la misma instancia de opciones para múltiples operaciones
SerializarConOpciones(opcionesGlobales);
}
static async Task ProcesarArchivoGrande(string archivo)
{
try
{
// Para archivos grandes, usar FileStream con buffer
using FileStream stream = File.OpenRead(archivo);
// Deserializar directamente desde el stream
var datos = await JsonSerializer.DeserializeAsync<Persona[]>(stream);
Console.WriteLine($"Procesadas {datos?.Length ?? 0} personas desde stream");
}
catch (FileNotFoundException)
{
Console.WriteLine("Archivo no encontrado, creando archivo de ejemplo...");
await CrearArchivoDatos(archivo);
}
}
static async Task CrearArchivoDatos(string archivo)
{
var personas = new Persona[]
{
new Persona { Nombre = "Usuario 1", Edad = 25, Email = "user1@test.com", Activo = true },
new Persona { Nombre = "Usuario 2", Edad = 30, Email = "user2@test.com", Activo = false },
new Persona { Nombre = "Usuario 3", Edad = 35, Email = "user3@test.com", Activo = true }
};
using FileStream stream = File.Create(archivo);
await JsonSerializer.SerializeAsync(stream, personas);
Console.WriteLine($"Archivo {archivo} creado con {personas.Length} registros");
}
static void SerializarConOpciones(JsonSerializerOptions opciones)
{
var datos = new { mensaje = "Hola", timestamp = DateTime.Now };
string json = JsonSerializer.Serialize(datos, opciones);
Console.WriteLine($"JSON optimizado: {json}");
}
}
Recomendaciones generales
Práctica | Descripción | Beneficio |
---|---|---|
Usar streams para archivos grandes | JsonSerializer.SerializeAsync() y DeserializeAsync() |
Menor consumo de memoria |
Reutilizar opciones | Crear JsonSerializerOptions una vez |
Mejor rendimiento |
Validar datos | Verificar propiedades requeridas después de deserializar | Mayor robustez |
Manejar excepciones | Capturar JsonException y otros errores |
Mejor experiencia de usuario |
Nombres consistentes | Usar PropertyNamingPolicy para convenciones |
Interoperabilidad |
Resumen
El trabajo con archivos JSON en C# es una habilidad fundamental para el desarrollo moderno. A través de System.Text.Json
, disponemos de herramientas potentes y eficientes para serializar objetos C# a formato JSON y deserializar datos JSON de vuelta a objetos tipados.
Hemos explorado desde operaciones básicas de lectura y escritura hasta técnicas avanzadas para manejar estructuras complejas, personalizar la serialización con atributos y implementar un manejo robusto de errores. La capacidad de trabajar eficientemente con JSON nos permite crear aplicaciones que se integran seamlessly con APIs web, manejan configuraciones flexibles y persisten datos de forma estructurada.
Las mejores prácticas que hemos cubierto, como el uso de streams para archivos grandes, la validación de datos y el manejo adecuado de excepciones, aseguran que nuestras aplicaciones sean robustas y eficientes. Con estas herramientas y conocimientos, estamos preparados para abordar cualquier desafío relacionado con el intercambio de datos JSON en nuestras aplicaciones C#.