Si activamos el soporte para Jackson, tal y como sugerí al principio, ahora tendremos activa la extensión de Quarkus REST Jackson (anteriormente RESTEasy Jackson), una de las más potentes que nos vamos a encontrar. Esta extensión es capaz de hacer introspección de los tipos de datos que devuelvan nuestros endpoints y serializar a JSON automáticamente con una precisión muy buena, incluso ajustarlo en el futuro si nos damos cuenta que alguna clase no se serializa como cabe esperar.
Si no se ha activado el soporte para Quarkus REST Jackson, se puede hacer con alguno de los siguientes tres comandos:
quarkus ext add io.quarkus:quarkus-rest-jackson, si tenemos la quarkus-cli../mvnw quarkus:add-extension -Dextensions="io.quarkus:quarkus-rest-jackson", si estamos usando Maven../gradlew addExtension --extensions="io.quarkus:quarkus-rest-jackson", si estamos usando Gradle.
Modelos
Típicamente lo que haremos con Quarkus será codificar a JSON nuestros modelos. Puede ser una entidad de base de datos o una clase de lógica de negocio.
Aquí tengo una clase denominada Temperatura:
package quarkus;
import java.util.Objects;
public class Temperatura {
private String ciudad;
private int minima;
private int maxima;
public Temperatura() {
}
public Temperatura(String ciudad, int minima, int maxima) {
this.ciudad = ciudad;
this.minima = minima;
this.maxima = maxima;
}
// Getters, setters, hashCode, equals y toString.
}
NOTA: Para no hacer tan largo el código, he quitado los getters, los setters, el método hashCode, equals y el toString. Habría que volver a ponerlos. Otras alternativas son usar Lombok, que permite generar dinámicamente esos métodos a partir de una clase que sólo contenga los campos, o bien usar records. Sí, hoy en día REST Jackson es compatible con records, por lo que podemos crear un registro y saber que se va a pasar bien a JSON.
Devolver JSON
Esta sección será breve: si ahora nuestros endpoints devuelven uno de estos modelos, podremos dejar que Jackson haga esa restrospección para generar una versión en JSON de nuestra instancia. Observa el siguiente código de un endpoint, el cual por brevedad es un fragmento únicamente del endpoint:
@GET
public Temperatura medicion() {
return new Temperatura("Málaga", 15, 25);
}
En este caso, nuestro endpoint devuelve directamente una instancia de Temperatura. Si visitamos este endpoint desde el navegador o desde un cliente REST como pueda ser Postman o Insomnia, veremos que lo ha convertido automáticamente a JSON:
{
"ciudad": "Málaga",
"minima": 15,
"maxima": 25
}
Poco más que decir. Jackson se ocupa de analizar, en función de los getters de la clase, qué propiedades tiene que incluir. Si tuviésemos un registro, haría análisis de los campos y sería eso lo que serializaría a JSON.
Una estrategia muy habitual hoy en día es tratar con objetos DTO (Data Transfer Object). Aquí es donde los records de Java encajan a la perfección. Podemos tener una entidad completa (una clase que tal vez sea de JPA), y proyectarla mediante una transformación en una instancia de un registro auxiliar que sólo contenga un subconjunto de la información a presentar.
También es posible crear modelos especiales que sólo se usan para proyectar propiedades. Imaginemos una entidad compleja como la siguiente
Person {
id: 5,
name: "John",
surname: "Doe",
company: {
id: 4,
name: "Exportadora de Alimentos SA",
country: "España",
createdAt: "2023-05-11T15:42:14"
},
createdAt: "2023-05-11T16:11:34"
}
Tal vez esto devuelva demasiada información si lo codificamos a JSON. Por eso hoy en día se ha puesto de moda el uso de este tipo de DTOs para ocultar la información que no sea relevante. En este caso tal vez sea de interés quitar campos como el id, o la fecha de creación, así como mostrar únicamente el nombre de la compañía en vez de toda su estructura de datos (con una proyección como company => company.name, ¡tened en cuenta que estoy hablando en pseudocódigo!):
PersonDto {
name: "John",
surname: "Doe",
company: "Exportadora de Alimentos SA"
}
Serializar a otras cosas que no son JSON
En este caso, está serializándose a JSON porque es lo que REST Jackson hace por defecto. Sin embargo, mediante la anotación @Produces podemos cambiar el tipo de medio que queremos que se genere. Aquí algunos ejemplos.
Si queremos serializar a JSON, podríamos poner tal cual el Content-Type que queremos que se devuelva:
@GET
@Produces("application/json")
public Temperatura medicion() {
return new Temperatura("Málaga", 15, 25);
}
Sin embargo, si queremos serializar a string, podemos ponerle text/plain:
@GET
@Produces("text/plain")
public Temperatura medicion() {
return new Temperatura("Málaga", 15, 25);
}
El serializador text/plain provocará que se llame al método toString(), lo cual puede ser útil para depurar información brevemente, aunque no sea fácil de consumir por ningún servicio.
Igualmente, tampoco es habitual que se ponga tal cual un string en la anotación Produces, sino que se use una constante como por ejemplo MediaType.TEXT_PLAIN o MediaType.APPLICATION_JSON. MediaType es una clase que forma parte del paquete jakarta.ws.rs.core y la podemos importar mediante import jakarta.ws.rs.core.MediaType;.
Serializar listas
Otro aspecto interesante de Jackson es que permite serializar cómodamente colecciones de datos y generar objetos u arrays JSON de una forma transparente. A modo de ejemplo, el siguiente endpoint devolverá una lista de temperaturas:
@GET
public List<Temperatura> mediciones() {
return Arrays.asList(
new Temperatura("Madrid", 15, 25),
new Temperatura("Barcelona", 24, 31),
new Temperatura("Bogotá", 4, 11)
);
}
En condiciones normales, este sería un método normal que devuelve una lista de tres elementos de tipo Temperatura. Pero cuando dejamos que Jackson reciba este objeto, lo que obtendremos por el endpoint será lo siguiente:
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
content-length: 136
[
{
"ciudad": "Madrid",
"maxima": 25,
"minima": 15
},
{
"ciudad": "Barcelona",
"maxima": 31,
"minima": 24
},
{
"ciudad": "Bogotá",
"maxima": 11,
"minima": 4
}
]
En definitiva, Jackson también es capaz de convertir listas a JSON para poder devolver conjuntos de elementos de forma cómoda.
Resumen
Este sería todo el código del controlador que hemos hecho en esta lección:
package quarkus;
import java.util.Arrays;
import java.util.List;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
@Path("/temperaturas")
public class TemperaturaResource {
@GET
@Produces(MediaType.APPLICATION_JSON)
public List<Temperatura> listado() {
return Arrays.asList(
new Temperatura("Madrid", 15, 25),
new Temperatura("Barcelona", 24, 31),
new Temperatura("Bogotá", 4, 11)
);
}
@GET
@Path("/una")
@Produces(MediaType.APPLICATION_JSON)
public Temperatura obtener() {
return new Temperatura("Málaga", 18, 28);
}
@GET
@Path("/una/string")
@Produces(MediaType.TEXT_PLAIN)
public Temperatura obtenerTexto() {
return new Temperatura("Málaga", 18, 28);
}
}