← 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

Distinguir el estado de HTTP del estado de la aplicación

¿Por qué se conserva la lista, si HTTP no recuerda nada?

HTTP trata cada petición como un intercambio independiente. Sin embargo, el programa puede conservar datos entre peticiones: hoy lo observarás al añadir una tarea a una lista y consultarla después.

No hay contradicción. Lo que no recuerda nada es el protocolo: la petición número 3 no sabe que existió la número 2. Pero el programa sigue vivo entre una y otra, con su memoria intacta, y tu ArrayList es un atributo de un objeto que Spring creó una sola vez al arrancar y reutiliza para todas las peticiones.

Compruébalo de la peor manera posible

Crea dos o tres tareas. Después para la aplicación y vuelve a arrancarla. Pide GET /tareas.

Vacío. Todo perdido. La memoria es del proceso, y el proceso ha muerto. El comportamiento no constituye un defecto del trabajo realizado, sino exactamente el problema que resuelve una base de datos, y por eso existe la UD5.

¿Por qué GET /tareas/999 no da error?

Pruébalo. Devuelve 200 y un cuerpo vacío, porque tu método devuelve null y Spring no tiene nada que serializar.

Está mal, y conviene que sepas por qué: le estás diciendo al cliente que todo ha ido bien cuando no has encontrado lo que pedía. Lo correcto sería un 404. Todavía no sabemos fijar el código de estado a mano —eso es la UD2—, así que hoy lo dejamos anotado como defecto conocido.

Se trabaja

140 minutos · implementación guiada sobre el proyecto propio

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

  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. Escoge tres campos de tu entidad principal y un registro de ejemplo. Escribe qué tipo Java corresponde a cada campo; usarás los mismos nombres al redactar el JSON.

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. Si lo adaptas a otra entidad, 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 librería que entró en el proyecto con spring-boot-starter-web 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 · Jackson lee los getters, no los atributos

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. Jackson nunca miró el atributo privado: recorrió los métodos públicos que empiezan por get o por is, les quitó ese prefijo y pasó a minúscula la primera letra.

Qué significa en la práctica
El JSON que ve el cliente lo determinan tus getters, no tus atributos. Renombrar un getter es un cambio visible desde fuera.
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 constituye el contenido central de la UD2. 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. 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. Comprueba en el navegador que los corchetes exteriores indican varios objetos y las llaves delimitan cada uno.

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

Recuerda importar java.util.List.

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

Prueba a devolver una tarea con la prioridad sin asignar:

return new Tarea(1, "Revisar el login", null, false);
{"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.

Paso 7 · Definir el modelo de tu propio dominio

Aplica la clase de ejemplo a tu entidad: 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(...). Crea un objeto en el endpoint /ejemplo y compara campo a campo su JSON con los valores del constructor antes de pasar al POST.

  1. Crea la clase Proyecto en el paquete model, con al menos: id, nombre, descripcion, activo y numeroDeIncidencias.
  2. Escribe un ProyectoController con dos rutas:
    • GET /proyectos devuelve una lista con tres proyectos inventados.
    • GET /proyectos/{id} devuelve uno solo, construido con el id recibido.
  3. Comprueba las dos en el navegador y 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 un atributo private String notaInterna sin escribir su getter. Reinicia, mira el JSON y explica en una frase por qué no aparece.

Postman y la primera escritura

Paso 8 · Postman, y solo lo imprescindible

Cliente HTTP

Un programa cuyo único trabajo es construir peticiones a mano y enseñarte la respuesta entera. Es al backend lo que el navegador al frontend: la ventana por la que ves lo que estás construyendo.

Usaremos Postman. Si prefieres Bruno, que es más ligero y guarda las peticiones como archivos dentro del proyecto, todo lo de hoy funciona igual y cambian los nombres de dos botones.

Hoy Postman es una herramienta, no un tema

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.

Postman tiene además colecciones, entornos, variables, scripts, ejecución automatizada y gestión de credenciales. Nada de eso se toca hoy. Todo eso llega en la UD2, cuando ya tengas peticiones que merezca la pena guardar y repetir. Aprender la herramienta antes de tener el problema que resuelve es la forma más rápida de olvidarla.

Descarga Postman de su web oficial e instálalo. Te pedirá crear una cuenta: puedes saltártelo, buscando el enlace pequeño de trabajar sin conexión. No necesitamos sincronizar nada.

Antes de probar nada nuevo, comprueba la herramienta con algo cuyo resultado ya conoces. Es una costumbre que te ahorrará muchas confusiones: si falla, sabrás que falla la herramienta y no tu código.

  1. Crea una petición nueva.
  2. Deja el método en GET.
  3. Escribe la URL: http://localhost:8080/tareas.
  4. Pulsa Send.

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. Es el mismo texto: lo único que cambia es que aquí se lee.

Antes de enviar POST, añade a TareaController el método temporal crear() de la demostración, dentro de la clase. Añade también import org.springframework.web.bind.annotation.PostMapping; junto a los demás imports. Guarda y reinicia. Cambia entonces el método de GET a POST en el desplegable, sin tocar la URL, y pulsa Send.

Alguien ha hecho un POST

Ese método que hace un minuto era inalcanzable acaba de ejecutarse. Eso es todo lo que Postman aporta hoy, y es suficiente para trabajar tres semanas.

Paso 9 · Recibir datos · @RequestBody

Un POST que no recibe nada sirve de poco. 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; al principio del archivo. Guarda y reinicia antes de enviar el JSON.

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

Este método, de momento, devuelve exactamente lo que recibe. Es un espejo, y es la mejor forma de comprobar que la entrada llega bien antes de hacer nada con ella.

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 con constructor vacío y setters, para esto hacía falta el constructor vacío. Jackson necesita poder crear el objeto antes de saber qué valores va a ponerle. Si borras ese constructor, este endpoint deja de funcionar.

Por la misma razón resultan necesarios los setters: al serializar, Jackson lee con los getters; al deserializar, escribe con los setters.

  1. Método POST, URL http://localhost:8080/tareas.
  2. Abre la pestaña Body, debajo de la URL.
  3. Marca la opción raw.
  4. En el desplegable de la derecha, que por defecto pone Text, elige JSON.
  5. Escribe el cuerpo:
{
  "id": 1,
  "titulo": "Revisar el login",
  "prioridad": "alta",
  "completada": false
}
  1. Send.

La respuesta devuelve el mismo objeto. Ha hecho un viaje completo: texto JSON, objeto Java, texto JSON otra vez.

El paso 4 es el que se olvida

Elegir JSON en ese desplegable no cambia el color del texto: hace que Postman envíe la cabecera Content-Type: application/json. Sin ella, tu servidor no sabe cómo interpretar el cuerpo y contesta 415 Unsupported Media Type.

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. Si ya adaptaste el modelo a tu dominio, mantén esos mismos nombres y campos.

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;
    }
}

Es Java corriente: una ArrayList, un bucle y un add. Toda la parte web son cinco anotaciones que ya conoces.

Ejecuta estas cuatro peticiones en este orden y ve prediciendo cada respuesta antes de pulsar Send:

# 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, para y date cuenta de lo que acabas de construir: una petición ha cambiado lo que devuelve otra. Eso ya es una aplicación, no un ejercicio.

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

Retoma el controlador de la otra entidad que preparaste en la sesión 2, por ejemplo ProyectoController. Aplica el procedimiento que acabas de realizar: crea su modelo, sustituye las respuestas de texto por objetos y listas y añade el POST que conserva los objetos en memoria. 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.
  3. Comprueba las tres en Postman siguiendo la misma secuencia de cuatro pasos de antes, y anota el código de estado de cada una.
  4. Envía un POST con un campo mal escrito a propósito y anota qué llega y qué responde.

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.

Objetivo mínimoLa clase Tarea y las rutas de ejemplo devolviendo JSON, con el Content-Type comprobado.
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 respuestas

1 · De los métodos públicos que empiezan por get o por is, quitándoles el prefijo y bajando a minúscula la primera letra. No de los atributos privados.

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.

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 mínimoPostman instalado, un GET repetido y un POST con cuerpo JSON que responde 200.
Si lo tienesLa lista en memoria funcionando en tareas y en proyectos, con la secuencia de cuatro peticiones comprobada.
RetoLas tres respuestas diagnosticadas y reproducidas, con dos causas distintas para la tercera.
Ver respuestas

1 · Porque crea el objeto primero, vacío, y solo después le asigna los valores llamando a los setters. Sin constructor sin argumentos no puede dar el primer paso.

2 · 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.

3 · 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.

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

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.