Hasta ahora, BiblioTech ha guardado y recuperado su propio estado: en texto plano, en JSON, y en una base de datos relacional. Pero ninguna aplicación real vive aislada: casi siempre necesita comunicarse con otros sistemas a través de la red, típicamente mediante una API REST —un servicio que expone datos y operaciones sobre HTTP, con JSON como formato habitual de intercambio. Esta lección profundiza en System.Text.Json para escenarios más complejos que los vistos hasta ahora, y presenta HttpClient, la clase de .NET para realizar peticiones HTTP, consumiendo un servicio externo (simulado) que devuelve metadatos adicionales de un libro a partir de su ISBN. Cierra así el Módulo 5 (Trabajando con Datos), la última pieza antes del Módulo 6, dedicado a temas más avanzados del lenguaje y la plataforma.

Contenido

  1. Repaso y profundización: System.Text.Json en escenarios más complejos
  2. Colecciones anidadas y opciones de nomenclatura con JsonNamingPolicy
  3. HttpClient: la puerta de entrada a servicios HTTP
  4. Consumir un GET y deserializar la respuesta
  5. Enviar datos con POST
  6. Buenas prácticas con HttpClient: reutilización e IHttpClientFactory
  7. Manejo de errores de red y HTTP
  8. BiblioTech consulta metadatos externos de un libro por ISBN

  1. Repaso y profundización: System.Text.Json en escenarios más complejos

La lección de Serialización presentó JsonSerializer.Serialize/Deserialize sobre objetos sencillos y colecciones planas. El JSON que devuelve una API externa real, sin embargo, suele tener una estructura más rica: objetos anidados dentro de otros objetos, listas dentro de un objeto, y convenciones de nombres que no coinciden con PascalCase. Esta lección retoma exactamente esos casos.

  1. Colecciones anidadas y opciones de nomenclatura con JsonNamingPolicy

Considera una respuesta típica de una API externa de metadatos de libros, con una lista anidada de géneros y un objeto anidado con datos de la editorial:

{
  "isbn": "978-84-376-0495-4",
  "editorial": { "nombre": "Sudamericana", "pais": "Argentina" },
  "generos": ["Novela", "Literatura latinoamericana"],
  "puntuacionMedia": 4.6
}

Para deserializar esta estructura, se modelan las clases anidadas tal cual reflejan el JSON:

class Editorial
{
    public string Nombre { get; set; } = string.Empty;
    public string Pais { get; set; } = string.Empty;
}

class MetadatosLibroExterno
{
    public string Isbn { get; set; } = string.Empty;
    public Editorial Editorial { get; set; } = new Editorial();
    public List<string> Generos { get; set; } = new List<string>();
    public double PuntuacionMedia { get; set; }
}

JsonSerializer.Deserialize<MetadatosLibroExterno>(json) reconstruye automáticamente tanto el objeto anidado (Editorial) como la lista (Generos), sin ningún código adicional: EF Core, en la lección anterior, y System.Text.Json, aquí, comparten la misma filosofía de mapear estructuras completas por convención.

Muchas APIs reales usan camelCase (puntuacionMedia, no PuntuacionMedia) en sus claves JSON, en vez del PascalCase habitual de las propiedades de C#. En lugar de anotar cada propiedad con [JsonPropertyName] una a una (visto en la lección de Serialización), JsonSerializerOptions.PropertyNamingPolicy aplica la conversión a todas las propiedades de golpe:

var opciones = new JsonSerializerOptions
{
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
    WriteIndented = true
};

MetadatosLibroExterno? metadatos = JsonSerializer.Deserialize<MetadatosLibroExterno>(json, opciones);
Console.WriteLine(metadatos?.Editorial.Nombre); // "Sudamericana"

string jsonGenerado = JsonSerializer.Serialize(metadatos, opciones);
// las claves se generan en camelCase: "isbn", "editorial", "generos", "puntuacionMedia"

PropertyNamingPolicy funciona en ambas direcciones (serializar y deserializar), y es la opción recomendada frente a [JsonPropertyName] cuando toda una clase sigue la misma convención de nombres; reserva [JsonPropertyName] para las excepciones puntuales dentro de una clase que, por lo demás, sigue la convención por defecto.

  1. HttpClient: la puerta de entrada a servicios HTTP

HttpClient (del namespace System.Net.Http) es la clase de .NET para realizar peticiones HTTP: GET para obtener datos, POST para enviarlos, y el resto de verbos HTTP habituales (PUT, DELETE...). Su uso básico consiste en crear una instancia, indicar (opcionalmente) una BaseAddress, y realizar peticiones contra rutas relativas a ella:

using System.Net.Http;

HttpClient cliente = new HttpClient
{
    BaseAddress = new Uri("https://api.bibliotech-externo.example/")
};

El paquete System.Net.Http.Json (incluido de serie en .NET moderno) añade métodos de extensión que combinan la petición HTTP con la deserialización JSON en una sola llamada: GetFromJsonAsync<T>, PostAsJsonAsync<T>, evitando el paso intermedio de leer el cuerpo de la respuesta como texto y deserializarlo aparte.

  1. Consumir un GET y deserializar la respuesta

using System.Net.Http.Json;

MetadatosLibroExterno? metadatos =
    await cliente.GetFromJsonAsync<MetadatosLibroExterno>("libros/978-84-376-0495-4");

if (metadatos is not null)
{
    Console.WriteLine($"Editorial: {metadatos.Editorial.Nombre} ({metadatos.Editorial.Pais})");
    Console.WriteLine($"Puntuacion media: {metadatos.PuntuacionMedia}");
}

GetFromJsonAsync<T> realiza la petición GET, comprueba que la respuesta fue satisfactoria, y deserializa el cuerpo JSON directamente al tipo T indicado —los tres pasos que, con JsonSerializer a secas y un HttpClient sin la extensión .Json, requerirían escribirse por separado (GetAsync, leer el cuerpo con ReadAsStringAsync, y JsonSerializer.Deserialize).

  1. Enviar datos con POST

Para enviar datos (por ejemplo, registrar en un servicio externo que BiblioTech ha añadido un libro nuevo al catálogo), PostAsJsonAsync<T> serializa el objeto indicado a JSON y lo envía como cuerpo de la petición:

class NuevoLibroExterno
{
    public string Isbn { get; set; } = string.Empty;
    public string Titulo { get; set; } = string.Empty;
}

NuevoLibroExterno nuevoLibro = new NuevoLibroExterno
{
    Isbn = "978-84-376-0497-8",
    Titulo = "El Aleph"
};

HttpResponseMessage respuesta = await cliente.PostAsJsonAsync("libros", nuevoLibro);

if (respuesta.IsSuccessStatusCode)
{
    Console.WriteLine("Libro registrado en el servicio externo.");
}

respuesta.IsSuccessStatusCode es true para cualquier código de estado HTTP 2xx (200, 201, 204...); es la forma habitual de comprobar, sin necesidad de leer el código numérico exacto, si una petición tuvo éxito.

  1. Buenas prácticas con HttpClient: reutilización e IHttpClientFactory

A diferencia de SqliteConnection o StreamReader, HttpClient no debe crearse con using en cada petición y desecharse inmediatamente después. Aunque HttpClient también implementa IDisposable, crear una instancia nueva por cada petición puede agotar los sockets disponibles del sistema operativo bajo carga (cada HttpClient desechado deja su conexión de red en un estado de cierre que tarda un tiempo en liberarse del todo):

Patrón Correcto para...
Una única instancia de HttpClient, reutilizada durante toda la vida de la aplicación (o de un componente de larga duración) Aplicaciones de consola y scripts sencillos, como los de este curso
IHttpClientFactory (inyectado mediante inyección de dependencias) Aplicaciones ASP.NET Core y otros escenarios con muchas peticiones concurrentes (Módulo 7)
new HttpClient() dentro de un using, en cada petición Evitar: puede agotar los sockets disponibles bajo carga sostenida

Para el alcance de este curso —una aplicación de consola como BiblioTech— basta con crear una única instancia de HttpClient (por ejemplo, como campo static readonly de la clase que la usa) y reutilizarla en todas las llamadas; IHttpClientFactory resuelve el mismo problema de forma más sofisticada en aplicaciones ASP.NET Core, donde el ciclo de vida de los componentes es distinto, un tema que se retoma en el Módulo 7 (Construcción de Aplicaciones).

  1. Manejo de errores de red y HTTP

Una petición HTTP puede fallar de dos formas muy distintas, y conviene distinguirlas:

Tipo de fallo Excepción / síntoma Ejemplo
Fallo de red HttpRequestException (u otra excepción de red) El servidor no responde, no hay conexión a Internet
Respuesta HTTP de error La petición se completa, pero con un código 4xx/5xx El recurso no existe (404), error del servidor (500)
try
{
    HttpResponseMessage respuesta = await cliente.GetAsync("libros/isbn-inexistente");
    respuesta.EnsureSuccessStatusCode(); // lanza HttpRequestException si el codigo no es 2xx

    MetadatosLibroExterno? metadatos =
        await respuesta.Content.ReadFromJsonAsync<MetadatosLibroExterno>();
}
catch (HttpRequestException ex)
{
    Console.WriteLine($"Error al consultar el servicio externo: {ex.Message}");
}
catch (TaskCanceledException)
{
    Console.WriteLine("La peticion ha superado el tiempo de espera.");
}

EnsureSuccessStatusCode() convierte un código de error HTTP en una excepción HttpRequestException, para poder tratarlo con el mismo try/catch que cualquier otro error (recordando el Manejo de Excepciones del Módulo 2), en vez de comprobar manualmente el código numérico en cada llamada. TaskCanceledException puede producirse si la petición supera el tiempo de espera configurado en HttpClient.Timeout, un escenario habitual cuando el servicio externo no responde a tiempo.

  1. BiblioTech consulta metadatos externos de un libro por ISBN

Uniendo todo lo anterior, Biblioteca gana un método que consulta un servicio externo (simulado, como el Task.Delay de la lección de Programación Asíncrona lo era para una verificación lenta) para enriquecer la información de un Libro con datos que BiblioTech no guarda por sí misma:

class Biblioteca
{
    // ... Catalogo, Socios, Prestamos, metodos anteriores del curso sin cambios ...

    private static readonly HttpClient ClienteHttp = new HttpClient
    {
        BaseAddress = new Uri("https://api.bibliotech-externo.example/")
    };

    private static readonly JsonSerializerOptions OpcionesJsonExterno = new JsonSerializerOptions
    {
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase
    };

    public async Task<MetadatosLibroExterno?> ObtenerMetadatosPorIsbnAsync(string isbn)
    {
        try
        {
            HttpResponseMessage respuesta = await ClienteHttp.GetAsync($"libros/{isbn}");
            respuesta.EnsureSuccessStatusCode();

            return await respuesta.Content.ReadFromJsonAsync<MetadatosLibroExterno>(OpcionesJsonExterno);
        }
        catch (HttpRequestException ex)
        {
            Console.WriteLine($"No se pudieron obtener metadatos externos para '{isbn}': {ex.Message}");
            return null;
        }
    }
}
Libro rayuela = new Libro("Rayuela", "Julio Cortazar", "978-84-376-0495-4");
biblioteca.AgregarMaterial(rayuela);

MetadatosLibroExterno? metadatos = await biblioteca.ObtenerMetadatosPorIsbnAsync(rayuela.Isbn);
if (metadatos is not null)
{
    Console.WriteLine($"'{rayuela.Titulo}' - Editorial: {metadatos.Editorial.Nombre}, puntuacion: {metadatos.PuntuacionMedia}");
}
else
{
    Console.WriteLine($"No hay metadatos externos disponibles para '{rayuela.Titulo}'.");
}

ObtenerMetadatosPorIsbnAsync sigue el mismo patrón que PrestarLibroAsync de la lección de Programación Asíncrona: un método async Task<T>, con su Async de rigor en el nombre, que encapsula una operación de E/S (antes, un Task.Delay simulado; ahora, una petición HTTP real) y maneja sus propios errores devolviendo null cuando la consulta falla, en vez de propagar la excepción hacia quien lo llama.

sequenceDiagram
    participant Main as Codigo cliente
    participant Bib as Biblioteca
    participant Http as HttpClient
    participant Api as API externa

    Main->>Bib: await ObtenerMetadatosPorIsbnAsync(isbn)
    Bib->>Http: GetAsync("libros/{isbn}")
    Http->>Api: GET /libros/{isbn}
    Api-->>Http: 200 OK + JSON
    Http-->>Bib: HttpResponseMessage
    Bib->>Bib: ReadFromJsonAsync<MetadatosLibroExterno>
    Bib-->>Main: MetadatosLibroExterno

Errores Comunes y Consejos

  • Crear un HttpClient nuevo por cada petición con using: bajo carga, agota los sockets disponibles del sistema; reutiliza una única instancia (o usa IHttpClientFactory en aplicaciones ASP.NET Core).
  • No distinguir un fallo de red de una respuesta HTTP de error: GetAsync no lanza una excepción por sí sola ante un 404 o un 500 —hay que llamar a EnsureSuccessStatusCode() (o comprobar IsSuccessStatusCode) explícitamente para tratarlos como error.
  • Asumir que el JSON externo sigue PascalCase: la mayoría de APIs reales usan camelCase o snake_case; configura PropertyNamingPolicy (o [JsonPropertyName] para casos puntuales) en vez de asumir que coincidirá con los nombres de las propiedades de C#.
  • Olvidar el manejo de errores en una llamada de red: a diferencia de leer un fichero local, una petición HTTP depende de un sistema externo que puede no responder, tardar demasiado, o devolver un error; todo código que use HttpClient en una aplicación real necesita su try/catch correspondiente.
  • Consejo: para depurar el JSON exacto que devuelve una API externa antes de escribir las clases de destino, es útil deserializar primero a un tipo genérico de inspección (como JsonDocument o incluso string con WriteIndented) y mirar su estructura real, en vez de adivinar las propiedades a ciegas.

Ejercicios

  1. Define las clases Editorial y MetadatosLibroExterno tal como se han presentado en esta lección. Deserializa el JSON de ejemplo del apartado 2 usando JsonNamingPolicy.CamelCase, y muestra por consola Editorial.Pais y el primer elemento de Generos.

  2. Escribe un método async Task<bool> ExisteLibroEnServicioExternoAsync(HttpClient cliente, string isbn) que haga un GetAsync($"libros/{isbn}") y devuelva true si respuesta.IsSuccessStatusCode es verdadero, false en caso contrario, sin lanzar ninguna excepción por un 404.

  3. Añade a Biblioteca el método ObtenerMetadatosPorIsbnAsync de esta lección. Llámalo desde un Main asíncrono para un libro del catálogo, y maneja tanto el caso de éxito (mostrando la editorial) como el caso en que el método devuelva null.

Soluciones

string json =
    """
    {
      "isbn": "978-84-376-0495-4",
      "editorial": { "nombre": "Sudamericana", "pais": "Argentina" },
      "generos": ["Novela", "Literatura latinoamericana"],
      "puntuacionMedia": 4.6
    }
    """;

var opciones = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
MetadatosLibroExterno? metadatos = JsonSerializer.Deserialize<MetadatosLibroExterno>(json, opciones);

Console.WriteLine(metadatos?.Editorial.Pais);   // "Argentina"
Console.WriteLine(metadatos?.Generos[0]);        // "Novela"
async Task<bool> ExisteLibroEnServicioExternoAsync(HttpClient cliente, string isbn)
{
    try
    {
        HttpResponseMessage respuesta = await cliente.GetAsync($"libros/{isbn}");
        return respuesta.IsSuccessStatusCode;
    }
    catch (HttpRequestException)
    {
        return false;
    }
}
Libro libro = biblioteca.Catalogo.OfType<Libro>().First();
MetadatosLibroExterno? metadatos = await biblioteca.ObtenerMetadatosPorIsbnAsync(libro.Isbn);

if (metadatos is not null)
{
    Console.WriteLine($"Editorial de '{libro.Titulo}': {metadatos.Editorial.Nombre}");
}
else
{
    Console.WriteLine($"No hay metadatos externos disponibles para '{libro.Titulo}'.");
}

OfType<Libro>() es un operador LINQ (de la lección de LINQ, Módulo 4) que filtra una colección quedándose solo con los elementos de un tipo concreto —aquí, solo los Libro del Catalogo mixto, descartando las Revista.

Conclusión

En esta lección has profundizado en System.Text.Json para estructuras anidadas y convenciones de nombres reales, y has aprendido a usar HttpClient para consumir una API REST externa: peticiones GET y POST, buenas prácticas de reutilización, y manejo diferenciado de errores de red frente a errores HTTP. Biblioteca ya puede enriquecer su catálogo con información de un servicio externo, cerrando así el Módulo 5 (Trabajando con Datos): desde el fichero de texto plano de la primera lección hasta una API REST externa, pasando por JSON, ADO.NET y Entity Framework, BiblioTech ha dejado de ser una aplicación que solo vive en memoria.

El Módulo 6 (Temas Avanzados) retoma, con más profundidad, varias herramientas que este módulo ya ha usado de pasada: la reflexión, que es literalmente el mecanismo que permite a JsonSerializer inspeccionar las propiedades de una clase sin código manual de mapeo; los atributos, como [JsonPropertyName] o [JsonDerivedType] vistos en la lección de Serialización; la gestión de memoria y el recolector de basura (Garbage Collector), que libera automáticamente los objetos que este módulo ha ido creando; y el multihilo real con Thread y Parallel, que completa lo que async/await dejó apuntado en el Módulo 4 sobre concurrencia.

Curso de Programación en C#

Módulo 1: Introducción a C#

Módulo 2: Estructuras de Control

Módulo 3: Programación Orientada a Objetos

Módulo 4: Conceptos Avanzados de C#

Módulo 5: Trabajando con Datos

Módulo 6: Temas Avanzados

Módulo 7: Construcción de Aplicaciones

Módulo 8: Mejores Prácticas y Patrones de Diseño

Módulo 9: Proyecto Final

© Copyright 2026. Todos los derechos reservados