← De Java a la Web: HTTP y Spring Boot

Sesión 3 · Semana 2

JSON y primera escritura con un cliente HTTP

Proyecto compartido. En el taller de Intermodular que abre esta semana has trabajado issues, tablero y la primera pull request. En Servidor continúas la implementación del mismo producto.

Se explica

25 minutos · explicación y demostración

Tus rutas ya reciben parámetros, pero responden con frases. Hoy responderán con datos estructurados en JSON y recibirán un objeto enviado por el cliente. Jackson es la biblioteca que convierte entre JSON y objetos Java; veremos ambas direcciones antes de guardar datos en una lista.

Devolver texto no escala

Hasta ahora tus métodos devuelven frases:

return "Ficha del usuario " + id;

Eso está bien para comprobar que una ruta responde, y no sirve para nada más. Imagina que otro programa recibe esto:

Tarea 3: Revisar el login, prioridad alta, sin terminar

Para saber la prioridad tendría que buscar la palabra «prioridad», contar comas y confiar en que nadie cambie nunca la redacción. El día que alguien escriba «Prioridad: alta» en lugar de «prioridad alta», el programa que lo lee se rompe.

Un backend no habla con personas: habla con programas, y los programas requieren datos estructurados, no frases.

{
  "id": 3,
  "titulo": "Revisar el login",
  "prioridad": "alta",
  "completada": false
}

Ahora la prioridad se pide por su nombre, y el orden, los espacios o la redacción dan igual.

JSON en cinco minutos

JSON

JavaScript Object Notation. Un formato de texto para representar datos estructurados. Nació en JavaScript, pero hoy lo entienden todos los lenguajes: es el idioma común de las APIs.

Solo tiene dos estructuras:

{ "clave": "valor" }

Un objeto: llaves, y dentro pares de clave y valor separados por comas. Las claves van siempre entre comillas dobles.

[ 1, 2, 3 ]

Un array: corchetes y valores separados por comas.

Los valores admiten seis tipos, incluidos otro objeto y otro array, que es lo que permite anidar cuanto haga falta:

Valor JSON Ejemplo Equivalente en Java
Cadena "alta" String
Número 3 o 2.5 int, long, double
Booleano true boolean
Nulo null null
Objeto { "id": 3 } Un objeto de una clase tuya
Array [1, 2] List, array

Los tres errores de sintaxis de todo el mundo

Comillas simples: { 'id': 3 } no es JSON. Siempre dobles.

Coma final: { "id": 3, } no es JSON. La última pareja no lleva coma.

Claves sin comillas: { id: 3 } es un objeto de JavaScript, no JSON.

Los tres producen el mismo resultado cuando los envíes en el trabajo siguiente: un 400, porque el servidor no consigue interpretar el cuerpo.

Por qué necesitamos un cliente que pueda enviar POST

En este ejemplo se utiliza un controlador temporal con solo este método bajo la ruta /tareas. Observa el resultado; crearás tu controlador durante la práctica:

@PostMapping
public String crear() {
    return "Alguien ha hecho un POST";
}

Al abrir http://localhost:8080/tareas en la barra del navegador, se envía GET. Si el controlador de la demostración solo admite POST, devuelve:

{
  "status": 405,
  "error": "Method Not Allowed",
  "path": "/tareas"
}

405 Method Not Allowed. La ruta existe, pero no con ese método.

El problema conviene enunciarlo con claridad: no existe forma de escribir una URL que provoque un POST. La barra de direcciones siempre hace GET. Siempre. No es una limitación que se pueda rodear con un truco.

Si existe también un método GET para esa ruta, el navegador ejecutará ese GET y no aparecerá el 405. Para elegir POST y enviar un cuerpo utilizaremos el cliente HTTP de la práctica.

Lo que puede pedir cada cliente
  1. Barra de direcciones: solo GET, sin cuerpo, sin cabeceras propias
  2. Cliente HTTP: cualquier método, con el cuerpo y las cabeceras que decidas

Se trabaja

140 minutos · implementación guiada sobre el proyecto propio

Paso 1 · Retomar el proyecto y preparar la comprobación

Prepara la rama antes de editar. En Intermodular 2 has protegido main también en este backend. Crea aquí una issue para añadir el modelo JSON y la primera escritura, con criterios que permitan comprobar el listado y el POST. Asígnatela. En el ejemplo su número es 12: sustitúyelo por el real en la rama y, al abrir la PR, en Closes #12.

Abre una terminal en la carpeta del backend, donde está pom.xml, y comprueba con git status que no quedan cambios pendientes. Con el trabajo anterior publicado en main, ejecuta:

git switch main
git pull --ff-only
git switch -c 12-json-y-escritura

Si todavía queda una PR de trabajo anterior sin integrar, completa primero su revisión e integración. Durante esta sesión guarda los cambios en esta rama; el procedimiento de ramas y revisión de Intermodular es el mismo para este código Java.

  1. Arranca el proyecto y abre una ruta de la sesión 2. Mantén el navegador para comprobar las primeras respuestas; instalarás y usarás Postman o Bruno en el paso dedicado al cliente HTTP.
  2. Crea el paquete model bajo tu paquete base y localiza el paquete controller. Las clases de datos irán en el primero y los métodos HTTP en el segundo.
  3. Identifica las dos entidades de tu proyecto y sus controladores. El recorrido utiliza Tarea y Proyecto: primero comprobarás la salida JSON de ambas; después añadirás la entrada y el almacenamiento a Tarea y aplicarás el mismo procedimiento a Proyecto.

Mantén los nombres de tu proyecto. Si elegiste otro dominio en la sesión 1, anota antes de seguir qué entidad corresponde a Tarea y cuál a Proyecto, junto con sus clases, campos y rutas. Sustituye esos nombres de forma consistente en el código, las URL y los cuerpos JSON. No necesitas crear un segundo proyecto Spring Boot ni entidades adicionales solo para copiar el ejemplo.

Localiza tu paquete base. Es el que aparece en la línea package de la clase principal con @SpringBootApplication. Los ejemplos usan com.ejemplo.gestor: si el tuyo es com.ejemplo.biblioteca, utiliza com.ejemplo.biblioteca.model y com.ejemplo.biblioteca.controller, cambia los imports y guarda los archivos bajo src/main/java/com/ejemplo/biblioteca/. Mantén los paquetes model y controller dentro de ese paquete base.

Durante la práctica, reiniciar significa guardar los archivos, detener la ejecución anterior y volver a arrancar la aplicación como en la sesión 1. Espera a que termine el arranque antes de enviar peticiones. Modificar el JSON en el cliente HTTP no requiere reiniciar el servidor.

Paso 2 · El modelo · una clase Java normal

Vamos a representar una tarea del gestor. Crea el paquete com.ejemplo.gestor.model y dentro la clase:

package com.ejemplo.gestor.model;

public class Tarea {

    private int id;
    private String titulo;
    private String prioridad;
    private boolean completada;

    public Tarea() {
    }

    public Tarea(int id, String titulo, String prioridad, boolean completada) {
        this.id = id;
        this.titulo = titulo;
        this.prioridad = prioridad;
        this.completada = completada;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getTitulo() {
        return titulo;
    }

    public void setTitulo(String titulo) {
        this.titulo = titulo;
    }

    public String getPrioridad() {
        return prioridad;
    }

    public void setPrioridad(String prioridad) {
        this.prioridad = prioridad;
    }

    public boolean isCompletada() {
        return completada;
    }

    public void setCompletada(boolean completada) {
        this.completada = completada;
    }
}

Esta clase utiliza Java sin anotaciones: cuatro atributos privados, dos constructores y métodos para leer y cambiar los valores. Guarda el archivo en src/main/java/com/ejemplo/gestor/model/Tarea.java, ajustando el paquete y la entidad según el paso 1. Conserva la correspondencia entre nombre de clase, archivo, atributos y métodos de acceso.

Fíjate solo en dos detalles, porque los dos van a importar:

  • El constructor vacío se utilizará más adelante en esta misma práctica para construir el objeto a partir del JSON recibido.
  • El getter de un boolean se llama isCompletada(), no getCompletada(). Es la convención de Java, y tiene consecuencias visibles dentro de un momento.
¿No sería más corto un record?

Un record Tarea(int id, String titulo, String prioridad, boolean completada) permite representar y serializar esos datos, pero no proporciona setters para modificarlos. Utilizamos aquí una clase mutable porque la modificaremos en la sesión 4 y la prepararemos para JPA en la UD5. Más adelante usaremos records para DTO.

Paso 3 · Devolver el objeto y ver qué pasa

Crea TareaController.java en src/main/java/com/ejemplo/gestor/controller con este contenido. Si ya lo creaste para el reto de la sesión 2, edita ese archivo: no declares una segunda clase con el mismo nombre.

package com.ejemplo.gestor.controller;

import com.ejemplo.gestor.model.Tarea;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/tareas")
public class TareaController {

    @GetMapping("/ejemplo")
    public Tarea ejemplo() {
        return new Tarea(1, "Revisar el login", "alta", false);
    }
}

Reinicia y abre http://localhost:8080/tareas/ejemplo:

{"id":1,"titulo":"Revisar el login","prioridad":"alta","completada":false}

Tú has devuelto un objeto Java y ha salido JSON. Nadie ha escrito una sola línea para convertirlo.

Mira además el panel de red: el Content-Type ya no es text/plain, es application/json. Spring ha cambiado también la cabecera, porque ha cambiado lo que devuelve.

Quién ha hecho la conversión

Recuerda el reparto de la sesión 1. Cuando tu método termina, Spring tiene un valor Java en la mano y tiene que meterlo en el cuerpo de la respuesta. Para eso usa Jackson, la biblioteca que entró en el proyecto con spring-boot-starter-webmvc sin que la pidieras.

De objeto Java a cuerpo de respuesta
  1. Tu método devuelve un Tarea
  2. Spring ve que es @RestController
  3. Jackson recorre sus getters
  4. Construye el texto JSON
  5. Se envía con Content-Type: application/json

Serializar

Convertir un objeto en memoria a un formato de texto que se pueda transmitir o guardar. Lo contrario —texto a objeto— es deserializar, y llega en el trabajo siguiente.

Paso 4 · Los getters y las claves del JSON en este modelo

El experimento se hace en el modelo, no en el controlador: cambia temporalmente un getter de Tarea.java, guarda, reinicia y consulta /tareas/ejemplo. Compara las claves del JSON con las que tenía antes. Restaura el getter al terminar para que los pasos de entrada de datos partan del modelo completo. No cambies simultáneamente el nombre del campo y el de la ruta.

En Tarea, renombra getTitulo() a getNombre(). No toques el atributo, que sigue llamándose titulo.

Escribe qué clave esperas ver en el JSON: ¿titulo o nombre?

{"id":1,"nombre":"Revisar el login","prioridad":"alta","completada":false}

La clave es nombre. En este modelo, con atributos privados y sin anotaciones de Jackson, los getters permiten detectar las propiedades de salida. Para estos nombres, getNombre() produce nombre e isCompletada() produce completada. Jackson también admite campos públicos, anotaciones y otras configuraciones; aquí comprobamos el comportamiento de la clase que acabas de escribir.

Qué significa en la práctica
En este ejemplo, renombrar un getter cambia una clave que ve el cliente, aunque conserves el nombre del atributo privado.
Por qué completada aparece bien
Porque para los boolean la convención es is, y Jackson también quita ese prefijo. Si lo hubieras llamado getCompletada(), la clave seguiría siendo completada. Si lo llamas estaCompletada(), sin prefijo reconocible, el campo desaparece del JSON sin ningún error.
El fallo típico que provoca
«Le he puesto el campo a la clase y no sale en el JSON.» Casi siempre es que falta el getter, o que no sigue la convención de nombres.

Deja getTitulo() como estaba antes de seguir.

Y si quiero que la clave se llame distinta al getter

Se puede, con @JsonProperty("titulo_tarea") sobre el getter. Hoy no lo usamos y conviene saber por qué: retocar el modelo para que el JSON salga bonito acaba mezclando dos cosas distintas —cómo guardas los datos y cómo los publicas—.

La solución adecuada consiste en declarar una clase independiente para lo que se publica. Se denomina DTO y se trabaja en la UD3. Hasta entonces el modelo se devuelve sin transformación.

Paso 5 · Devolver varias tareas

Añade el método de listado al mismo TareaController, conservando /ejemplo. Si ya tienes un método con @GetMapping sin ruta adicional, sustituye ese método: solo debe quedar uno que atienda GET /tareas. Importa java.util.List al principio del archivo. Esta primera lista contiene objetos escritos en el código para observar un array JSON; en el paso 11 se sustituirá por una lista mutable compartida entre peticiones.

@GetMapping
public List<Tarea> lista() {
    return List.of(
        new Tarea(1, "Revisar el login", "alta", false),
        new Tarea(2, "Actualizar dependencias", "baja", true)
    );
}

Guarda y reinicia. Abre http://localhost:8080/tareas y comprueba que los corchetes exteriores indican una lista y las llaves delimitan cada objeto.

GET /tareas responde:

[{"id":1,"titulo":"Revisar el login","prioridad":"alta","completada":false},
 {"id":2,"titulo":"Actualizar dependencias","prioridad":"baja","completada":true}]

El navegador lo mostrará todo seguido en una línea. No se trata de un defecto: nadie ha solicitado que la salida se formatee. En Chrome y Firefox tienes una pestaña de visualización de JSON que lo ordena, y en el trabajo siguiente Postman te lo dará indentado y coloreado.

Objeto o array: la decisión importa

Una ruta que devuelve una cosa —/tareas/3— devuelve un objeto JSON. Una ruta que devuelve un conjunto —/tareas— devuelve un array, y lo devuelve aunque solo haya un elemento, y aunque no haya ninguno.

Un array vacío se escribe []. Nunca null, y nunca un texto diciendo «no hay tareas»: quien te llama espera una lista y sabe perfectamente recorrer una lista de cero elementos.

Paso 6 · Cuando un campo vale null

En TareaController.java, localiza el método ejemplo() del paso 3 y sustituye únicamente su línea return por esta. Conserva el método lista() del paso 5:

return new Tarea(1, "Revisar el login", null, false);

Guarda, reinicia y abre http://localhost:8080/tareas/ejemplo. La respuesta debe contener:

{"id":1,"titulo":"Revisar el login","prioridad":null,"completada":false}

La clave aparece con el valor null: el campo está presente y no tiene valor. Distingue ese caso de omitir la clave y de enviar una cadena vacía "". No son lo mismo, aunque una configuración concreta pueda tratarlos de forma equivalente. Volveremos a esta distinción al validar entradas en la UD3. Al terminar, restaura "alta" en esa línea, guarda y reinicia para continuar con el ejemplo inicial.

Paso 7 · Preparar la salida JSON de la segunda entidad

Conserva Tarea.java y TareaController.java: volverás a ellos en el paso 8. Ahora prepara la segunda entidad, Proyecto en el ejemplo, en archivos independientes. Escribe primero sus campos y tipos, después el constructor vacío, el constructor con datos y los métodos de acceso. Revisa que una propiedad nombre tenga getNombre() y setNombre(...).

  1. Crea la clase Proyecto en el paquete model, con al menos: id, nombre, descripcion, activo y numeroDeIncidencias.
  2. En el paquete controller, crea ProyectoController o edita el que ya preparaste en la sesión 2. Utiliza @RestController y @RequestMapping("/proyectos"). Sustituye las respuestas de texto de estas dos rutas por objetos, sin duplicar sus métodos:
    • GET /proyectos devuelve una lista con tres proyectos inventados.
    • GET /proyectos/{id} devuelve uno solo, construido con el id recibido.
  3. Guarda, reinicia y abre http://localhost:8080/proyectos y http://localhost:8080/proyectos/1. Compara las claves y los valores del JSON con los objetos que construiste. Anota, del panel de red, el código de estado y el Content-Type.
  4. Escribe en un comentario qué claves exactas tiene tu JSON y de qué método sale cada una.

Añade después a Proyecto.java un atributo private String notaInterna sin escribir su getter. Reinicia, consulta /proyectos/1 y explica en una frase por qué no aparece. Después retira ese atributo experimental. Deja los dos archivos de Proyecto guardados: en el paso 12 les añadirás el almacenamiento. Los pasos 8–11 continúan sobre TareaController.

Postman y la primera escritura

Paso 8 · Preparar y enviar tu primera petición en Postman

Cliente HTTP

Una aplicación que permite elegir el método HTTP, la dirección y los datos de una petición, enviarla y examinar la respuesta del servidor. Hasta ahora escribías una dirección en el navegador y este enviaba un GET. Con un cliente HTTP también puedes preparar un POST con datos en su cuerpo.

Usaremos Postman para los ejemplos. Puedes utilizar Bruno para enviar las mismas peticiones: elige método y URL, selecciona cuerpo JSON y observa estado, cabeceras y cuerpo de respuesta. En la sesión 7 encontrarás las instrucciones específicas de ambas herramientas para variables y pruebas; sus scripts no son intercambiables.

Operaciones básicas del cliente HTTP

Vamos a dedicarle veinte minutos y vamos a aprender cuatro cosas: elegir el método, escribir la URL, enviar un cuerpo JSON y leer la respuesta.

En la sesión 7 organizarás las peticiones en colecciones y utilizarás variables y pruebas. Hoy basta con enviar cada petición y comprender su resultado.

Instala la aplicación de escritorio de Postman. Hoy puedes utilizar el cliente ligero sin iniciar sesión para enviar peticiones. En la sesión 7 necesitarás una cuenta y un espacio de trabajo para colecciones y entornos, o utilizar Bruno siguiendo su recorrido. Mantén el historial de las peticiones válidas para recuperarlas después.

Mantén el servidor arrancado. Antes de probar nada nuevo, consulta desde Postman el listado que ya funciona en el navegador. Si las respuestas difieren, compara el método, la URL y el puerto antes de cambiar el código.

  1. Pulsa + o Create new request y selecciona HTTP como tipo de petición. Según la versión, el botón puede estar en la barra lateral o junto a las pestañas. Si aparece New, utiliza New → HTTP. Debes llegar a un editor con un selector de método, un campo de dirección y el botón Send.
  2. Localiza el selector de método a la izquierda del campo de dirección y selecciona GET.
  3. En el campo de dirección escribe solo http://localhost:8080/tareas. No escribas GET dentro de ese campo: el método ya está seleccionado aparte.
  4. Deja Body → none, porque esta consulta no necesita enviar un cuerpo.
  5. Pulsa Send y espera la respuesta. Escribir la dirección o cambiar el método no envía nada por sí solo.

Abajo aparece la respuesta. Localiza estas cuatro cosas, que son las mismas de la sesión 1 y ahora se ven mucho mejor que en el navegador:

Dónde mirar Qué es
Arriba a la derecha del panel inferior El código de estado: 200 OK
Junto a él El tiempo que ha tardado y el tamaño de la respuesta
Pestaña Body El cuerpo, con el JSON ya indentado y coloreado
Pestaña Headers Las cabeceras de respuesta, con el Content-Type entre ellas

Compara ese JSON con el que veías en el navegador: debe contener las mismas tareas. Distingue las dos zonas de Postman: en el editor de la petición preparas lo que envías; en el panel de respuesta lees lo que devuelve el servidor. Ambas tienen una pestaña llamada Body, pero solo el cuerpo de la petición sirve para enviar datos.

Antes de enviar POST, abre TareaController.java. Conserva ejemplo() y lista() y añade este método dentro de la clase, antes de su última llave. Si ya copiaste el método de la demostración, utiliza ese; no lo dupliques.

@PostMapping
public String crear() {
    return "Alguien ha hecho un POST";
}

Añade import org.springframework.web.bind.annotation.PostMapping; junto a los demás imports. Guarda, reinicia el servidor y espera a que termine de arrancar. Ahora prepara el envío:

  1. En la misma pestaña de Postman, cambia el selector de GET a POST.
  2. Conserva la dirección http://localhost:8080/tareas. GET y POST pueden compartir dirección porque el método HTTP también determina qué método Java se ejecuta.
  3. En el editor de la petición, deja Body → none. Este método temporal no recibe datos.
  4. Pulsa Send una vez. En el panel de respuesta debes ver 200 OK y el texto siguiente:
Alguien ha hecho un POST

Has comprobado que Spring ha ejecutado crear(): la respuesta contiene exactamente el texto de su return. Elegir POST no guarda una tarea automáticamente; el resultado depende del código de ese método. Si recibes 405 Method Not Allowed, comprueba que has añadido @PostMapping y reiniciado. Si ves un listado JSON, revisa que el selector no siga en GET.

Paso 9 · Recibir datos · @RequestBody

Ahora vas a enviar los datos de una tarea. El cuerpo de la petición es el texto que acompaña al método y a la URL; aquí lo escribirás en JSON. Prepara primero cómo se construye el objeto y después el método que lo recibirá.

En Tarea.java, añade import com.fasterxml.jackson.annotation.JsonCreator; debajo de la línea package. Localiza el constructor vacío que ya existe y añade la anotación justo encima; no crees otro constructor vacío:

@JsonCreator
public Tarea() {
}

@JsonCreator indica a Jackson qué constructor debe utilizar al leer JSON. Nuestra clase tiene dos: el vacío y el que recibe todos los campos. Seleccionamos explícitamente el vacío para crear el objeto y asignar después los valores mediante sus setters. Esto evita que Jackson 3, utilizado por Spring Boot 4, elija el constructor con argumentos y rechace un cuerpo que omita un valor primitivo, como el id que en la próxima sesión asignará el servidor. Conserva el constructor con argumentos: lo siguen utilizando los ejemplos de GET escritos en Java.

Ahora abre TareaController.java y sustituye el método temporal crear() por el siguiente; no conserves los dos con la misma ruta POST. Añade import org.springframework.web.bind.annotation.RequestBody; junto a los imports del controlador. Guarda ambos archivos y reinicia antes de enviar el JSON.

@PostMapping
public Tarea crear(@RequestBody Tarea tarea) {
    return tarea;
}

Este método devuelve los datos que recibe sin guardarlos. @RequestBody Tarea tarea indica que Spring debe convertir el cuerpo recibido en una instancia de Tarea y entregársela al método. return tarea permite comprobar en la respuesta qué valores han llegado.

Deserializar

Lo contrario de la serialización que acabas de observar: convertir el texto JSON que llega en el cuerpo de la petición en un objeto Java. También lo hace Jackson.

De cuerpo de petición a objeto Java
  1. Llega POST /tareas con un cuerpo JSON
  2. Spring ve @RequestBody
  3. Jackson crea un Tarea con el constructor vacío
  4. Llama a los setters que correspondan a cada clave
  5. Pasa el objeto ya montado a tu método

En este modelo, el constructor vacío marcado con @JsonCreator permite crear primero la instancia y asignarle después los valores recibidos. Conserva esa anotación para seguir este procedimiento. No es un requisito universal de Jackson: otras clases pueden usar records o constructores configurados para recibir las propiedades, pero aquí no necesitas cambiar la forma de construcción.

En el modelo de este ejercicio ofrecemos getters para leer y setters para escribir. Jackson también puede utilizar campos y otras formas de construcción según su configuración: quitar un setter no garantiza que una propiedad deje de admitirse. Mantén los accesos del ejemplo y comprueba el JSON recibido y devuelto.

  1. En Postman, selecciona el método POST y conserva la URL http://localhost:8080/tareas.
  2. Abre la pestaña Body del editor de la petición, debajo de la URL. No utilices el Body del panel de respuesta.
  3. Marca raw, que permite escribir el cuerpo como texto.
  4. En el selector de formato junto a esa opción, elige JSON; puede aparecer inicialmente como Text.
  5. En el cuadro de texto que aparece, pega únicamente el objeto siguiente, desde { hasta }. No lo escribas en un archivo Java ni en el campo de dirección. Los nombres deben coincidir con las propiedades de tu modelo; si trabajas con otro dominio, adapta los campos manteniendo sus tipos.
{
  "id": 1,
  "titulo": "Revisar el login",
  "prioridad": "alta",
  "completada": false
}
  1. Comprueba antes de enviar: método POST, dirección terminada en /tareas, formato JSON y cuerpo con comillas dobles en los nombres. false va sin comillas porque es un booleano.
  2. Pulsa Send una vez. En el panel de respuesta, localiza el estado y abre su Body para leer el JSON devuelto.

La respuesta debe mostrar 200 OK y devolver los mismos valores en JSON, aunque cambien el orden de las claves o los espacios. Por ejemplo, debes reconocer "titulo": "Revisar el login" y "completada": false. Has enviado texto JSON, el servidor lo ha convertido en un objeto Java y ha convertido ese objeto en el JSON de respuesta.

Para comprobar que controlas el envío, cambia en el cuerpo de la petición el título por Preparar la reunión y pulsa Send de nuevo: la respuesta debe mostrar el nuevo título. Después recupera el cuerpo del ejemplo. Por ahora también envías id; en la sesión 4 lo asignará el servidor.

Comprueba la diferencia entre responder y guardar: abre otra pestaña HTTP, selecciona GET, escribe http://localhost:8080/tareas, deja Body → none y pulsa Send. Todavía aparecen los dos objetos escritos en el código, no la tarea que acabas de enviar. Conserva las dos pestañas para alternar la escritura y la consulta. El almacenamiento se añade en el paso 11.

Formato del cuerpo y cabecera Content-Type

Elegir JSON hace que Postman añada la cabecera Content-Type: application/json a la petición. Esa cabecera informa al servidor del formato del cuerpo; no convierte en JSON válido un texto mal escrito. Puedes comprobarla en Headers de la petición, mostrando las cabeceras automáticas si están ocultas.

Compruébalo ahora: cambia el desplegable a Text, envía, y mira el error. Después vuelve a dejarlo en JSON. Ese 415 te va a pasar de verdad, y así lo reconocerás.

Paso 10 · Tres formas de romperlo, y qué contesta cada una

Pruébalas las tres. Anota el código y quédate con el patrón.

Qué envías Respuesta Por qué
Cuerpo con una coma de más 400 No es JSON válido, Jackson no puede leerlo
Content-Type sin poner 415 El servidor no acepta un cuerpo de ese tipo
{"titulo": "Algo", "color": "azul"} 200 Ojo con esta

La tercera merece detenerse. color no existe en la clase Tarea, y aun así la petición funciona: Spring Boot está configurado para ignorar en silencio las claves que no reconoce.

El mismo comportamiento se produce ante una errata. Envía esto:

{
  "titulo": "Revisar el login",
  "prioridadd": "alta"
}

Responde 200, y la prioridad llega como null. Nadie te avisa de nada.

Recuerda esto para la UD3

Lo que un cliente te envía no está comprobado. Ahora mismo tu API acepta una tarea sin título, con prioridad nula, con el id que le dé la gana al cliente y con campos inventados. No se queja porque nadie le ha dicho todavía qué es una tarea válida.

Eso se llama validación de entrada, y es un tema entero. Hasta entonces, trabaja siempre con la sospecha de que lo que llega puede ser cualquier cosa.

Paso 11 · Guardar las tareas en memoria

Hasta ahora el POST devolvía el objeto recibido sin almacenarlo. Sustituye el contenido de TareaController.java por esta versión, que conserva una lista entre peticiones. El ejemplo incluye los imports y todos los métodos necesarios: no lo pegues dentro de la clase anterior. Mantén el paquete y los nombres de tu dominio acordados en el paso 1. Conserva el modelo Tarea.java y los archivos de la segunda entidad.

Este cambio sustituye el listado fijo y el POST anterior, añade la consulta por identificador y retira el método /ejemplo. Desde ahora comprobarás GET /tareas, POST /tareas y GET /tareas/{id}. La dirección /tareas/ejemplo deja de ser una prueba válida.

package com.ejemplo.gestor.controller;

import com.ejemplo.gestor.model.Tarea;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.List;

@RestController
@RequestMapping("/tareas")
public class TareaController {

    private final List<Tarea> tareas = new ArrayList<>();

    @GetMapping
    public List<Tarea> lista() {
        return tareas;
    }

    @GetMapping("/{id}")
    public Tarea detalle(@PathVariable(name = "id") int id) {
        for (Tarea tarea : tareas) {
            if (tarea.getId() == id) {
                return tarea;
            }
        }
        return null;
    }

    @PostMapping
    public Tarea crear(@RequestBody Tarea tarea) {
        tareas.add(tarea);
        return tarea;
    }
}

La lista tareas se declara como atributo de la clase, fuera de los métodos, para que el POST y los dos GET accedan a los mismos datos. tareas.add(tarea) incorpora el objeto recibido; el listado devuelve la lista y el bucle de detalle() busca una tarea por su identificador. Si declararas una lista nueva dentro de cada método, no compartirían los registros.

final impide asignar otra lista al atributo tareas; permite añadir, sustituir y eliminar elementos de la lista existente. Por eso tareas.add(tarea) es válido.

Guarda y reinicia una vez antes de empezar. En Postman, utiliza la base http://localhost:8080. Para el POST, recupera el JSON válido del paso 9, con "id": 1, y comprueba Body → raw → JSON. Ejecuta estas cuatro peticiones en este orden, sin reiniciar ni editar código entre ellas, y predice cada respuesta antes de pulsar Send. En esta versión las cuatro responden con 200 OK:

# Petición Qué debe pasar
1 GET /tareas [], la lista vacía
2 POST /tareas con la tarea 1 Devuelve la tarea creada
3 GET /tareas Ahora sale un array con una tarea
4 GET /tareas/1 Sale esa tarea sola, como objeto

Cuando la cuarta responda, comprueba la diferencia con el paso 9: el POST ha cambiado los datos que devuelven los GET posteriores. Antes de pasar a la segunda entidad, verifica que recuperas el título y la prioridad que acabas de enviar.

Cada vez que pulses Send en la pestaña POST, crear() añadirá otro objeto. Para consultar lo que ya existe, utiliza la pestaña GET; reenviar un POST no equivale a actualizar la vista. Si lo has enviado varias veces por error, reinicia y repite la secuencia desde la lista vacía.

Estado de la aplicación y pérdida de datos al reiniciar

HTTP trata cada petición como un intercambio independiente, pero la aplicación puede conservar datos entre peticiones. Con la configuración de este ejercicio, Spring reutiliza la misma instancia del controlador y su lista mientras la aplicación sigue ejecutándose.

Detén ahora la aplicación, vuelve a arrancarla y envía GET /tareas. Debe devolver []: la lista se crea de nuevo al arrancar y los datos anteriores solo estaban en la memoria del proceso. La persistencia en una base de datos se incorporará en la UD5. Vuelve a enviar el POST válido si necesitas recuperar la tarea para otra comprobación.

Consulta de un identificador inexistente

Envía GET http://localhost:8080/tareas/999, asegurándote de no haber creado una tarea con ese identificador. Observa el estado 200 OK y el cuerpo vacío. El bucle termina sin encontrar la tarea y el método devuelve null.

Anótalo como defecto conocido: la respuesta debería ser 404 Not Found para indicar que el recurso no existe. En la UD2 aprenderás a establecer ese estado. Distingue este cuerpo vacío del array [] que devuelve un listado sin elementos.

Diagnóstico de errores durante las comprobaciones
Resultado observadoPrimera comprobación
Conexión rechazada o ausencia de respuesta HTTPComprueba que la aplicación haya terminado de arrancar y que la URL utilice el puerto mostrado en la consola. Si el puerto 8080 está ocupado, detén la ejecución anterior antes de arrancar otra.
La aplicación no arranca y muestra Ambiguous mappingBusca dos métodos con la misma combinación de ruta y método HTTP. Al sustituir un ejemplo, elimina la versión anterior; GET y POST sí pueden compartir la ruta.
404 Not Found en una ruta que debería existirCompara la URL con @RequestMapping y la anotación del método. Comprueba también que el controlador esté dentro del paquete base y que hayas reiniciado tras editarlo.
400 Bad Request al enviar el POSTRevisa comillas dobles, comas y tipos de los valores del cuerpo. Recupera primero el JSON válido del paso 9.
400 al omitir el id u otro campo primitivoSi la consola menciona Cannot map null into type int o boolean, comprueba que el constructor vacío del modelo lleva @JsonCreator, con su import, y que has reiniciado. En este ejercicio seleccionamos ese constructor para recibir los datos mediante setters.
415 Unsupported Media TypeSelecciona Body → raw → JSON y comprueba que la petición envía Content-Type: application/json.
POST correcto, pero el listado no contiene la tareaComprueba que crear() ejecute tareas.add(tarea), que ambos métodos usen la lista declarada como atributo y que no hayas reiniciado entre peticiones.

Paso 12 · Aplicar el patrón a la segunda entidad de tu proyecto

Retoma Proyecto.java y ProyectoController.java, que dejaste preparados en el paso 7. Conserva los campos y accesos del modelo. En Proyecto.java, añade el import de JsonCreator y marca su constructor vacío con @JsonCreator, como acabas de hacer en Tarea.java, para utilizar el mismo procedimiento de lectura del JSON. En su controlador, aplica el almacenamiento que acabas de comprobar con Tarea; mantén también funcionando el controlador de tareas. Usa estos criterios para comprobarlo:

  1. Sustituye la lista inventada por un ArrayList vacío, como atributo del controlador.
  2. Deja funcionando GET /proyectos, GET /proyectos/{id} y POST /proyectos. El listado debe devolver la lista compartida, el detalle debe buscar en ella y el POST debe añadir el objeto recibido. Sustituye el detalle inventado del paso 7 por esa búsqueda.
  3. Guarda y reinicia. Comprueba las tres rutas en Postman siguiendo la misma secuencia de cuatro pasos de antes, sin reiniciar entre envíos, y anota el código de estado de cada una. Utiliza un cuerpo JSON con los campos de Proyecto, no los de Tarea.
  4. Envía un POST con un campo mal escrito a propósito y anota qué llega y qué responde.

Para el modelo propuesto en el paso 7, puedes empezar con este cuerpo en POST http://localhost:8080/proyectos y recuperar después el registro con GET http://localhost:8080/proyectos/1:

{
  "id": 1,
  "nombre": "Gestor de tareas",
  "descripcion": "Organizar las tareas del equipo",
  "activo": true,
  "numeroDeIncidencias": 0
}

Ajusta las claves y los valores si tu segunda entidad pertenece a otro dominio. Al terminar, ambos controladores deben conservar y consultar sus propios registros.

Paso 13 · Comprobar y registrar el resultado del proyecto

  1. Envía un POST con un objeto JSON válido y consulta el listado con GET sin reiniciar. Debe aparecer el objeto; en esta primera versión el identificador aún puede venir del cliente.
  2. Reinicia y vuelve a consultar: los datos añadidos desaparecen porque estaban en memoria. Conserva la petición válida y otra con JSON mal formado, que debe producir 400.

Ampliación si has completado el trabajo

Primero termina y verifica los pasos anteriores. Estos retos profundizan en el mismo contenido; no sustituyen la entrega ni obligan a iniciar otro proyecto.

Reto · Predice el JSON

Dada esta clase, y sin ejecutarla, escribe el JSON exacto que produciría new Incidencia(7, "Caída del servidor", 3):

public class Incidencia {

    private int id;
    private String titulo;
    private int prioridad;
    private String autor;

    public Incidencia(int id, String titulo, int prioridad) {
        this.id = id;
        this.titulo = titulo;
        this.prioridad = prioridad;
    }

    public int getId() {
        return id;
    }

    public String getTitulo() {
        return titulo;
    }

    public int getNivel() {
        return prioridad;
    }

    public String autor() {
        return autor;
    }

    public boolean isUrgente() {
        return prioridad >= 3;
    }
}

Presta atención a las cuatro trampas: hay un getter renombrado, un método sin prefijo, un atributo sin getter y un getter que no corresponde a ningún atributo. Cuando lo tengas escrito, cópiala al proyecto y compruébalo.

Comprobación del retoEl JSON predicho coincide con la respuesta y puedes explicar cada propiedad.
Si lo tienesEl modelo Proyecto completo con sus dos rutas, y explicado por qué el campo sin getter no aparece.
RetoEl JSON de Incidencia predicho entero antes de ejecutarlo, con las cuatro trampas identificadas.
Ver la solución del reto

El JSON es {"id":7,"titulo":"Caída del servidor","nivel":3,"urgente":true}.

El getter renombrado: getNivel() devuelve el atributo prioridad, y la clave que sale es nivel. El nombre del atributo no interviene.

El método sin prefijo: autor() no empieza por get ni por is, de modo que no se reconoce como getter.

El atributo sin getter: autor no aparece en el JSON por la razón anterior, y además valdría null, porque el constructor no lo asigna.

El getter sin atributo: isUrgente() no corresponde a ningún campo y aun así produce la clave urgente, con el valor calculado true. Lo que se publica son los métodos, no el estado.

Reto · Diagnóstico de tres respuestas

Un compañero te enseña estas tres respuestas de su API y te pregunta qué le pasa. Para cada una, escribe la causa más probable y qué le pides que compruebe, sin ver su código:

  1. Hace un POST y recibe 415.
  2. Hace un POST, recibe 200, y en el JSON de vuelta todos los campos están a null o a 0 menos uno.
  3. Hace un POST y recibe 200, pero el GET siguiente devuelve [].

Después provoca las tres en tu proyecto para confirmar tus hipótesis. La tercera es la más interesante: hay al menos dos formas distintas de conseguirla.

Objetivo esencial de la sesiónLas dos entidades de tu proyecto admiten POST y conservan registros en memoria, consultables mediante listado y detalle. Has comprobado un JSON mal formado, un tipo de contenido incorrecto y la pérdida de datos al reiniciar.
Comprobación adicionalUn segundo registro conserva datos distintos del primero y ambos se recuperan por su id sin confundir sus respuestas.
RetoLas tres respuestas diagnosticadas y reproducidas, con dos causas distintas para la tercera.
Ver la solución del reto

1 · Falta la cabecera Content-Type: application/json: el servidor no acepta un cuerpo cuyo tipo desconoce. Comprueba el desplegable del cuerpo en Postman, que debe estar en JSON y no en Text.

2 · Las claves del JSON no coinciden con los setters del modelo, casi siempre por una errata o por un campo renombrado. Las claves que no se reconocen se descartan en silencio, y el único campo que llega es el único escrito bien. Pídele el JSON enviado y la lista de setters de su clase.

3 · Hay dos causas posibles. La primera: el método del POST devuelve el objeto recibido sin añadirlo a la lista. La segunda: la aplicación se ha reiniciado entre las dos peticiones, y la lista vive en la memoria del proceso. Pídele que repita las dos peticiones seguidas sin reiniciar, para distinguir un caso del otro.

Cierre

15 minutos · resultado comprobable y explicación individual

Al terminar la sesión:

Las peticiones de alta y consulta funcionan sin editar el código entre envíos. Debe ser posible explicar la conversión entre JSON y Java, y por qué los datos se pierden al reiniciar. En la sesión 4 el servidor pasará a asignar el identificador.

Cada integrante explica una decisión del código apoyándose en una de las comprobaciones realizadas.

Comprobación de lo aprendido · 3 minutos, sin mirar los apuntes

  1. ¿De dónde salen los nombres de las claves del JSON?
  2. ¿Qué hace falta para que un campo de tu clase aparezca en la respuesta?
  3. ¿Qué devuelve una ruta de listado cuando no hay ningún elemento?
  4. ¿Qué significa serializar?
  5. ¿Por qué el modelo necesita un constructor sin argumentos?
  6. ¿Qué cabecera debe acompañar a un POST con cuerpo JSON, y qué ocurre si falta?
  7. ¿Qué hace el servidor con una clave que no existe en la clase?
  8. ¿Qué se pierde al reiniciar la aplicación, y por qué?
Ver respuestas

1 · En el modelo de esta práctica, de los getters: getTitulo() produce titulo e isCompletada() produce completada. Otras configuraciones o anotaciones de Jackson pueden cambiar cómo se detectan y nombran las propiedades.

2 · Que tenga getter y que su nombre siga la convención. Un método llamado autor() o estaCompletada() no lo es, y el campo desaparece sin ningún error.

3 · Un array vacío: []. Nunca null ni un mensaje de texto.

4 · Convertir un objeto que está en memoria en texto transmisible, en nuestro caso JSON.

5 · En el modelo utilizado, permite crear el objeto antes de asignarle los valores recibidos. Es la forma de construcción elegida para esta práctica; Jackson también admite otras, como records o constructores configurados para recibir las propiedades.

6 · Content-Type: application/json. Si falta, el servidor responde 415 Unsupported Media Type. En Postman se pone sola al elegir JSON en el desplegable del cuerpo.

7 · Se ignora en silencio, sin error y sin aviso. Por eso una errata en un nombre de campo deja ese valor a null y la petición parece correcta.

8 · Todo lo guardado. La lista vive en la memoria del proceso, y al reiniciar el proceso se crea de nuevo, vacía.

Antes de cerrar, publica la rama y abre su PR. Comprueba que estás en la rama creada al inicio, con su número real de issue, y que target/ está excluido por .gitignore.

git status
git add .
git diff --cached
git commit -m "Anadir modelo JSON y primera escritura"
git push -u origin 12-json-y-escritura

Revisa el contenido preparado con git diff --cached antes de confirmar. Abre la PR hacia main, incluye Closes #12 con el número real y explica qué peticiones permiten comprobar el listado y la escritura. Tu pareja revisa la rama y ejecuta esas peticiones en su copia del backend; debe detener cualquier otra aplicación que ocupe el puerto 8080 antes de arrancarla. Corrige sobre la misma rama si hace falta y fusiona después de la revisión. Todavía no se exige un despliegue público ni el check de HTML del portfolio.

Los commits deben reflejar cambios comprensibles durante el trabajo; no necesitas limitarte a uno al final de la sesión. El resultado evaluable es el código y sus comprobaciones en Servidor, y el recorrido de la issue, la rama y la revisión en Intermodular.