La llegada de Spring Boot 4 supone un salto sustancial en el ecosistema de aplicaciones basadas en este fantástico y muy usado framework. Parece que el equipo se ha puesto las pilas, incorporando bastantes cambios y mejoras en su pila, de lo cual me alegro. La competencia está siendo feroz y, desde luego, buena para mejorar.
Voy a resumir el pequeño problema que me he encontrado al actualizar uno de mis "pet projects" desde la versión 3.5.x a la 4.0.2. Lo cual me ha servido como práctica por si, llegado el momento, hubiese que actualizar alguno de los proyectos de la empresa.
Pila tecnológica de partida:
- Spring Boot 3.5.x (última versión disponible de esta rama)
- Java 21
- Spring Data Rest
- Dependencias normales necesarias: JPA, H2, openapi, actuator,...
- Front (vanilla javascript, aunque no cuenta para este post y fue implementada 95% aplicando vibe coding)
Objetivo: Migrar a Spring Boot 4.0.2 y que todo siga funcionando (dejaré para otra ocasión java 25).
El procedimiento a seguir ha sido similar a cualquier otra actualización, aunque al tratarse de un proyecto pequeño y de bajo impacto (solo me afectaba a mi) he sido liviano en algunos pasos, sabedor de que algo iba a fallar, pero no me importaba en exceso el tiempo de solucionarlo:
- Revisar documentación: Guía de migración, notas de lanzamiento.
- Preparar y validar test de aceptación (los unitarios ya estaban, me centré en test de integración y e2e).
- Proceder a la migración.
- Ejecutar y validar las pruebas de aceptación.
Como se puede ver, el proyecto es pequeño, pero suficiente para tropezarme con problemas y descubrir soluciones.
El problema
Tras completar los 3 pasos definidos anteriormente, leer y entender la modularización en Spring Boot 4, adaptar el pom.xml con las dependencias correctas, no tuve que realizar ningún cambio significativo en el código fuente. Lo cual me causó grata satisfacción. El proyecto compila y se ejecuta sin ningún contratiempo a la vista.
Paso 4: Ejecutar y validar las pruebas de aceptación.
Aquí vienen cuando lo matan. Resulta que los cambios en la biblioteca de Jackson (que está migrando a la versión 3) me estaba rompiendo la API Rest. Me explico a continuación.
Tras ejecutar varias pruebas, la aplicación no obtiene los resultado esperados. No hay fallo, no hay excepción ni error, simplemente no muestra los resultado esperados (lo que estaba bien con la versión 3.5.x ahora ya no lo está).
El problema, algunos de los endpoints de la API Rest no estaban devolviendo los objetos tal y como los esperaba el cliente, ni como la documentación (openapi) lo indicaba. ¡Se había roto la API!
Por ejemplo: en la versión anterior un recurso devuelve un objeto como el siguiente:
{
"productName": "Sulfato Piedra",
"productId": "F00031",
"productFamily": "FUNGICIDAS",
"containerFormat": "25 kg",
"year": 2025,
"month": "JULY",
"totalQuantitySold": 3000
}
Tal y como lo indica en su documentación OpenApi. Pero al actualizar la versión lo que está devolviendo es:
{
"productName": "Sulfato Piedra",
"productId": "F00031",
"productFamily": "FUNGICIDAS",
"containerFormat": "25 kg",
"year": 2025,
"month": 6,
"totalQuantitySold": 3000
}Si nos fijamos el atributo month ahora es un entero, antes una cadena. El caso es que está serializando java.time.Month como entero, en vez de como cadena. A pesar de que la documentación de la API indica cadena. ¿A qué se puede deber esto? parece un bug en Jackson o en la documentación.
La solución
Después de revisar y probar diferentes opciones, incluso preguntando a la IA (que me lió más que ayudó), creo que la migración a Jackson 3, y el mentener la retro-compatibilidad en Spring Boot 4 con Jackson 2, me estaba dando más problemas que soluciones.
Pues resulta que si, que hay cambios en este sentido, y que el equipo de Jackson ya lo tiene documentado en el JSTEP 2. La solución adoptada, y aunque me obliga a implementar un Serializador se la agradezco a dbouclier y su issue.
El Serializador quedaría así:
import org.springframework.boot.jackson.JacksonComponent;
import tools.jackson.core.JsonGenerator;
import tools.jackson.databind.SerializationContext;
import tools.jackson.databind.ValueSerializer;
import java.time.Month;
@JacksonComponent
class MonthSerializer extends ValueSerializer<month> {
@Override
public void serialize(Month value,
JsonGenerator generator,
SerializationContext context) {
generator.writeString(value.name());
}
}
Solución alternativa
Otra opción podría ser implementar un método getter en el DTO para exponer el atributo "month" y devolver el name del enum java.time.Month. Por razones obvias esta opción no me gusta mucho, pero ahí queda.
@Data
public class ProductSalesSummaryDto implements Serializable {
private String productName;
private String productId;
private String productFamily;
private String containerFormat;
private Integer year;
// 1. Ocultamos el campo original a la serialización JSON
// Lombok generará el getMonth(), pero Jackson lo ignorará por esta anotación.
@JsonIgnore
private Month month;
private BigDecimal totalQuantitySold;
// 2. Creamos un método explícito para el JSON que devuelve String
// Jackson usará el valor de retorno de este método para la propiedad "month"
@JsonProperty("month")
public String getMonthName() {
if (this.month == null) return null;
return this.month.name(); // Devuelve "JULY"
}
}
Bueno pues hasta aquí la experiencia. Por supuesto, como siempre, lo recomendable migrar hacia Spring Boot 4 y java 25, pero no a lo loco :o)

Comentarios