1. Línea base JSON fija sin burocracia
Si alguna vez has montado un mueble IKEA sin instrucciones, ya casi entiendes para qué sirve una línea base. Aunque tienes las piezas (DTO), las manos (controladores) y el destornillador (Spring MVC), sin un claro «sistema de coordenadas» el resultado puede convertirse en… un objeto de arte inesperado. En las API ocurre lo mismo: sin una línea base JSON fija, el contrato comienza a «flotar» y el cliente sufre.
Una línea base fija es un acuerdo que ahorra nervios tanto a ti como a todos los que usarán tu API. En nuestro curso, este acuerdo es simple: generamos JSON con Jackson 3 y trabajamos con Spring Boot 4. No porque «esté de moda», sino porque el curso debe ser internamente coherente: los ejemplos de código, el comportamiento de serialización, los errores típicos, el formato de fechas, el comportamiento de los enum… todo debe ser predecible y uniforme.
Un problema muy práctico para un principiante es el siguiente: buscas en Google «cómo devuelve Spring JSON», encuentras un tutorial de 2017, copias un fragmento de configuración y, de repente, tu proyecto empieza a comportarse de forma extraña. No porque seas mal desarrollador, sino porque has mezclado dos épocas distintas y dos líneas diferentes de bibliotecas. Como resultado, no estás aprendiendo REST API, sino haciendo arqueología de dependencias.
Para «fijar esto», ten en mente una tabla sencilla (no para memorizar, sino como orientación):
| Pregunta | Qué consideramos norma en el curso | Por qué es importante |
|---|---|---|
| ¿Qué biblioteca se encarga del JSON? | Jackson 3 | Un conjunto predecible de reglas de serialización/deserialización |
| ¿Dónde funciona esto? | Spring Boot 4 + Spring MVC | Todo el curso trata sobre la capa web/API sobre esta plataforma |
| ¿Cómo se forma el JSON? | Devolvemos objetos Java (DTO) | El contrato es visible en el código, no oculto en cadenas |
| Qué NO hacemos | No armamos JSON manualmente con cadenas | Es frágil, ilegible y apenas se puede probar correctamente |
Y la idea clave de esta lección: DTO es la forma del contrato, y Jackson es el mecanismo que convierte esa forma en JSON. Por eso Jackson no es para nosotros una «biblioteca secundaria allá abajo», sino parte de la disciplina de API.
2. Jackson en Spring MVC: su lugar en la cadena
En esta etapa es fácil caer en el pensamiento mágico: escribes un @RestController, devuelves un DTO, y el JSON «aparece como por arte de magia» en la respuesta. Spring realmente lo hace cómodo… pero no porque allí viva un duende JSON, sino porque existe un mecanismo muy concreto para procesar el cuerpo de la respuesta. Es importante mantener esto como base; sin ella, el JSON empieza a parecer magia.
Hablando de forma muy práctica, para nosotros es importante una cadena: el método del controlador devuelve un objeto Java → luego la capa web lo convierte en bytes en el cuerpo de la respuesta HTTP. Y si ese objeto debe convertirse en JSON, en esta cadena participa necesariamente Jackson.
Aquí hay un esquema simplificado (sin entrar en detalles internos del framework):
flowchart TD
C[Cliente HTTP] -->|"GET /api/v1/tasks/{taskId}"| D[Spring MVC]
D --> R[Método @RestController]
R --> DTO[Response DTO]
DTO --> J[Jackson 3]
J --> B[Bytes JSON en el cuerpo de la respuesta]
B --> C
Observa: el controlador en este esquema no escribe JSON. Devuelve un objeto — y ese es un punto de ingeniería clave. Tu controlador es el lugar donde eliges qué DTO es el contrato público. Y Jackson es la biblioteca que, según las reglas de serialización, convierte ese DTO en texto JSON.
Para sentir esto en la práctica, creemos un pequeño endpoint de demostración:
import com.example.tasktracker.api.dto.response.TaskSummaryResponse;
import com.example.tasktracker.domain.model.TaskStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
class DemoController {
@GetMapping("/api/v1/demo/task")
TaskSummaryResponse demoTask() {
// Devolvemos DTO, no una cadena: Spring MVC generará JSON mediante Jackson
return new TaskSummaryResponse("t-1", "Write docs", TaskStatus.TODO);
}
}
// Jackson generará JSON aproximadamente así: {"id":"t-1","title":"Write docs","status":"TODO"}
El sentido del ejemplo es que no escribes JSON en absoluto. Describes la estructura de la respuesta a través del tipo Java TaskSummaryResponse, y a partir de ese tipo Java se deriva automáticamente el formato JSON.
Aquí necesitamos un soporte: una única línea base y una única ruta normal para generar JSON. Tú diseñas el DTO, y Jackson lo convierte honestamente en JSON.
3. La línea Jackson 3 en el proyecto
Cuando el proyecto es pequeño, parece: «¿qué más da qué versión de Jackson?». Pero tan pronto como empiezas a desarrollar la API, de repente te das cuenta de que mezclar versiones y enfoques produce efectos muy extraños: el IDE se queja de incompatibilidad, al iniciar te encuentras un error inesperado o el comportamiento de serialización «no es como en el ejemplo». Por eso debemos establecer una disciplina simple: Jackson debe ser exactamente de una línea — Jackson 3.
En Spring Boot, la mayor parte de la «conexión JSON» la obtienes de fábrica a través del web-starter. Es cómodo, pero la comodidad tiene su lado oscuro: si agregas manualmente otro Jackson (o una versión antigua), puedes crearte una «doble base». Como una casa sobre dos pilotes incompatibles: se mantendrá, pero da miedo.
En un proyecto de aprendizaje, normalmente basta con que Jackson llegue a través del web-stack. Un ejemplo mínimo de fragmento build.gradle.kts (acortado para que se vea lo principal):
dependencies {
// Spring MVC + JSON (Jackson) viene del starter, normalmente no agregamos Jackson por separado
implementation("org.springframework.boot:spring-boot-starter-webmvc")
// Pila de pruebas de Spring Boot; las versiones y la compatibilidad se obtienen del BOM
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
Lo importante aquí no es que memorices las líneas, sino la regla: no agregamos bibliotecas JSON «por si acaso» si ya están presentes. Un escenario especialmente peligroso es «voy a agregar jackson-databind manualmente porque en el tutorial lo hacen así». En el desarrollo real se puede hacer, pero entonces estás obligado a vigilar la compatibilidad de versiones. En un proyecto de aprendizaje casi siempre es un punto de fallo innecesario.
Si quieres un método práctico de autocomprobación (sin profundizar en la gestión de dependencias), puedes ver qué versión de Jackson se ha incorporado mediante Gradle. Por ejemplo, así (es un comando, no código):
# Muestra qué versión de Jackson está realmente presente en el grafo de dependencias del proyecto
./gradlew dependencyInsight --dependency jackson-databind
Si ves que se ha incorporado la línea 3.x, estás dentro de la línea base. Si ves de repente 2.x, casi con toda seguridad significa que alguien «trajo de Internet» un fragmento de dependencia antiguo al proyecto. En ese momento es mejor parar y arreglarlo, mientras el proyecto es pequeño; luego será más doloroso.
Por separado, diré algo importante en tono de «Capitán Obvio, pero gracias»: no usamos Gson (ni ninguna «otra biblioteca JSON adicional») en este curso. No porque Gson sea malo, sino porque el curso se basa en un único comportamiento JSON y en un stack típico de Spring MVC. Cuantas menos alternativas tengas, más rápido aprenderás.
4. Controlador: DTO en lugar de cadena JSON
En programación existe la tentación de «hacerlo rápido y directo». Y uno de los movimientos «rápido y directo» más engañosos en REST API es devolver desde el controlador una cadena que «parece JSON». A primera vista funciona; a la segunda, empieza a romper el contrato, el mantenimiento y, en general, la fe en la humanidad.
El camino normal en nuestro proyecto es el siguiente: el controlador devuelve un DTO, y el JSON se genera automáticamente en el límite web. Por ejemplo (simplificado):
import com.example.tasktracker.api.dto.response.TaskSummaryResponse;
import com.example.tasktracker.domain.model.TaskStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
class TaskDebugController {
@GetMapping("/api/v1/tasks/{taskId}/summary")
TaskSummaryResponse summary(@PathVariable String taskId) {
// taskId lo tomamos de la URL, y devolvemos un DTO (contrato), no un modelo interno
return new TaskSummaryResponse(taskId, "Write docs", TaskStatus.TODO);
}
}
¿Qué tiene de bueno esto? El contrato se ve directamente en la firma del método: el endpoint devuelve TaskSummaryResponse. Jackson toma ese DTO y lo escribe en JSON. El IDE, el compilador y la refactorización habitual siguen trabajando con el tipo, no con un literal de cadena.
Si en lugar de eso devuelves una cadena con llaves, el contrato queda oculto dentro del texto. Ese JSON no se refactoriza junto con el código, es fácil romperlo sintácticamente, y el Content-Type y el formato de la respuesta empiezan a depender de decisiones manuales. Por lo tanto, la regla básica es simple: el controlador devuelve un DTO, no una cadena JSON.
Y por la misma razón, es mejor no exponer el modelo interno «tal cual». Jackson no sabe qué es contrato público y qué es detalle interno de implementación. Simplemente serializa lo que le das.
5. JSON predecible a partir del tipo Java
Aquí es útil adquirir el hábito: mirando un DTO, ser capaz de anticipar la forma JSON de la respuesta. No con el principio de «ahora lo ejecuto y veo», sino con el principio de «entiendo qué le prometo al cliente».
Al principio basta con tener en cuenta algunas correspondencias simples: un DTO o record se convierte en un objeto JSON, List<T> en un array, enum en una cadena, números y boolean en sus valores JSON habituales. Esto ya es suficiente para no escribir JSON manualmente y no tener que adivinar qué verá exactamente el cliente.
En el contexto de la Task Tracker API, esto significa algo simple: si un endpoint devuelve TaskSummaryResponse, debes ser capaz de decir qué campos y en qué forma general saldrán al exterior. Jackson no lo saca del aire ni de la «magia de Spring», sino de tu modelo Java.
Con este soporte ya es más fácil discutir los puntos delicados del contrato: cómo se lee el JSON de entrada, en qué se diferencia una fecha de un instante exacto, por qué enum y null se comportan no «de cualquier manera», sino según reglas perfectamente comprensibles.
6. Errores típicos al fijar la línea base Jackson
En este tema, los errores casi nunca son «no escribí bien la anotación», sino «tomé una decisión incorrecta a nivel de hábito». Por eso los errores parecen cotidianos e incluso «lógicos», pero luego cuestan caro. Ahora repasemos los más frecuentes, para que los reconozcas por el olor antes de que empiecen a vivir en tu proyecto como cucarachas en una residencia.
Error n.º 1: mezclar Jackson 2 y Jackson 3 porque «así está en el tutorial».
Es un clásico del género: encuentras un ejemplo, agregas la dependencia manualmente y se cuela en el proyecto otra línea de bibliotecas. A veces se manifiesta de inmediato (error de compilación), a veces con un comportamiento extraño en tiempo de ejecución. El remedio es simple y aburrido: mantener una única línea base, no añadir dependencias JSON sin motivo y, en caso de duda, mirar qué hay realmente en el classpath.
Error n.º 2: devolver una cadena JSON desde el controlador «por simplicidad».
Al principio parece incluso «más claro»: aquí está el JSON, se ve a simple vista. Pero en la práctica has perdido la parte más fuerte de Java: los tipos y el soporte de las herramientas. Una cadena no se refactoriza, el compilador no la verifica y se rompe fácilmente por una coma de más. Para un API REST de aprendizaje y de tipo producción es un mal hábito de base.
Error n.º 3: pensar que JSON significa «menos código», por lo que los DTO no son necesarios.
Cuando alguien escribe una cadena con JSON, efectivamente tiene menos clases. Pero tiene más caos: el contrato está disperso por los controladores, los errores se copian, los cambios se vuelven arriesgados y el API deja de ser uniforme. En nuestro proyecto, los DTO no son un adorno, sino la forma del contrato que se puede mantener y desarrollar.
Error n.º 4: percibir Jackson como «algo ahí abajo» y no vincularlo con el contrato.
Si no vinculas DTO y JSON, te sorprenderás regularmente: por qué un campo apareció, desapareció o se ve «diferente». En realidad, Jackson hace exactamente lo que se deduce de la forma de tu tipo Java. Por eso el pensamiento correcto es: «cambio el DTO, cambio el contrato JSON».
Error n.º 5: intentar «configurar Jackson a mi gusto» de inmediato, sin fijar el comportamiento por defecto.
Esto es especialmente tentador después de un par de conflictos con el formato de fecha o el nombre de un campo. Pero si no has fijado el modelo básico de «el controlador devuelve un objeto → el JSON se forma en el límite», una configuración temprana se convierte en una lucha caótica contra los síntomas. Por ahora nuestra tarea es entender y aceptar la línea base; más adelante será más fácil hablar de los detalles del contrato JSON sin magia innecesaria.
GO TO FULL VERSION