Jackson en Spring WebFlux
En WebFlux, Jackson es el motor invisible que convierte JSON en tus data classes y viceversa, hasta que necesitas ajustarlo, y entonces recurres a un único bean ObjectMapper.
Spring WebFlux es la pila web reactiva y no bloqueante construida sobre Project Reactor, y Jackson es la biblioteca JSON que hace el trabajo pesado por debajo. Cuando llega una petición con cuerpo JSON, WebFlux no te entrega un string crudo: le pide a un codec registrado, el Jackson2JsonDecoder, que lea el flujo reactivo de bytes y lo materialice en tus tipos Kotlin. A la salida, el Jackson2JsonEncoder serializa tu valor de retorno (un solo objeto, un Flux o un Mono) de vuelta a bytes JSON. Como Spring Boot autoconfigura todo esto, el controlador típico nunca menciona a Jackson; declaras una función que devuelve una data class y el framework cablea la (de)serialización por ti.
En el flujo kotlin-jackson, la dependencia clave es com.fasterxml.jackson.module:jackson-module-kotlin (incluida automáticamente por spring-boot-starter-webflux a través del starter de Kotlin). Este módulo le enseña a Jackson a leer data classes de Kotlin: resuelve los nombres de los parámetros del constructor, respeta los tipos no nulos y honra los valores por defecto, así no necesitas un constructor sin argumentos ni trucos con lateinit. Sin él, Jackson no ve los nombres de los parámetros del constructor de Kotlin y la deserialización de data classes inmutables falla. El módulo es lo que hace que el Kotlin idiomático —propiedades val, constructores primarios, valores por defecto— encaje limpiamente con el JSON entrante.
En controladores basados en corutinas lees el cuerpo con la extensión suspendida awaitBody<T>(). Un ServerRequest expone bodyToMono<T>() para el estilo reactivo, pero awaitBody<T>() suspende la corutina hasta que el cuerpo completo se decodifica y devuelve un T plano, que se lee como código secuencial normal. El parámetro de tipo reificado es importante: awaitBody<CreateUserRequest>() lleva el tipo genérico completo en tiempo de ejecución, así Jackson deserializa correctamente incluso formas genéricas como List<OrderLine>. Si el cuerpo pudiera faltar, awaitBodyOrNull<T>() devuelve null en lugar de lanzar una excepción. Estas extensiones viven en org.springframework.web.reactive.function.server y combinan de forma natural con funciones handler suspend.
La mayoría de las veces los valores por defecto bastan, pero los sistemas reales necesitan configurar cómo se ve y se parsea el JSON. El gancho más limpio en una app Spring Boot es un bean Jackson2ObjectMapperBuilderCustomizer: Boot construye el ObjectMapper por ti y aplica cada customizer, de modo que tus ajustes se fusionan con los valores por defecto sensatos de Spring (el módulo de Kotlin, el JavaTimeModule para java.time y la desactivación de fechas como timestamps). Dentro de un customizer puedes registrar módulos, fijar una estrategia de nombres como snake_case, ignorar propiedades desconocidas o controlar la inclusión de nulos. Recurre a un @Bean fun objectMapper(): ObjectMapper completo solo cuando realmente necesites reemplazar el mapper por entero, y recuerda que WebFlux lo descubre mediante el cableado de CodecCustomizer, no por la ruta de message-converters de MVC.
Un punto sutil pero vital: los codecs JSON que usa WebFlux no son automáticamente la misma instancia que un bean que definas a menos que la autoconfiguración de Boot lo recoja. Con los valores por defecto de Spring Boot y un customizer, el mismo ObjectMapper configurado respalda tanto al encoder como al decoder, así que tu estrategia de nombres y registros de módulos aplican uniformemente a cuerpos de petición y respuestas. Si construyes tu propio ObjectMapper a mano y lo necesitas en los codecs, regístralo mediante un CodecCustomizer o un WebFluxConfigurer que sobrescriba configureHttpMessageCodecs. Mantenerte con el enfoque del customizer evita por completo esta trampa.
El streaming de JSON es donde WebFlux realmente brilla. Devolver un Flux<T> desde un handler deja que el framework serialice y descargue elementos a medida que se producen, en lugar de almacenar toda la colección en memoria. Si el cliente acepta application/json, Spring emite un único arreglo JSON, transmitiendo cada elemento conforme llega. Si en cambio produces application/x-ndjson (JSON delimitado por saltos de línea) o text/event-stream, cada ítem se codifica y descarga de forma independiente: ideal para feeds de larga duración, exportaciones grandes o server-sent events. Jackson codifica cada elemento bajo demanda, así que una exportación de un millón de filas nunca vive entera en el heap.
El manejo de errores y la validación merecen una mención. Cuando Jackson no puede mapear un cuerpo —un payload malformado, un campo no nulo faltante o una discrepancia de tipos— WebFlux lanza una ServerWebInputException (a menudo envolviendo una DecodingException), que se traduce en un HTTP 400 en lugar de un 500. Puedes capturarlas en un @ControllerAdvice con @ExceptionHandler o, en el modelo funcional, en el filtro de tu router. Como el módulo de Kotlin impone la nulabilidad, un objeto JSON al que le falta un campo requerido se rechaza en tiempo de deserialización, dándote un contrato fuerte y declarativo: si deserializó, tus campos no nulos de Kotlin realmente están poblados.
Juntando todo, el modelo mental es simple. Spring Boot autoconfigura un único ObjectMapper de Jackson, lo comparte entre el encoder y el decoder de WebFlux, y lo expone para personalizar mediante Jackson2ObjectMapperBuilderCustomizer. Tus controladores se mantienen limpios —awaitBody<T>() de entrada, una data class o un Flux de salida— mientras el framework transmite bytes de forma reactiva. Solo tocas Jackson directamente cuando necesitas una estrategia de nombres personalizada, un serializador o tipos de medios de streaming, y aun así el cambio vive en un solo lugar. Domina ese único punto de configuración y el resto del manejo de JSON en WebFlux se vuelve predecible.
data class CreateUserRequest(val name: String, val email: String)data class UserResponse(val id: Long, val name: String, val email: String)class UserHandler(private val service: UserService) {suspend fun create(request: ServerRequest): ServerResponse {// Suspends until the full body is decoded; reified T drives Jacksonval body = request.awaitBody<CreateUserRequest>()val user = service.create(body.name, body.email)return ServerResponse.status(HttpStatus.CREATED).bodyValueAndAwait(UserResponse(user.id, user.name, user.email))}}
A coroutine handler in the functional model: awaitBody<T>() decodes the JSON body into a Kotlin data class, and the returned object is serialized back to JSON automatically. The Kotlin module makes the immutable data class work without a no-arg constructor.
@Configurationclass JacksonConfig {@Beanfun jacksonCustomizer(): Jackson2ObjectMapperBuilderCustomizer =Jackson2ObjectMapperBuilderCustomizer { builder ->builder.propertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)builder.featuresToDisable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)builder.serializationInclusion(JsonInclude.Include.NON_NULL)// KotlinModule + JavaTimeModule are added by Boot automatically}}
Configure the single auto-configured ObjectMapper for the whole app via a customizer bean. These settings apply to BOTH the WebFlux encoder and decoder, so requests and responses use snake_case and tolerate unknown fields.
@RestControllerclass MetricsController(private val repo: MetricRepository) {// Single streamed JSON array for normal clients@GetMapping("/metrics", produces = [MediaType.APPLICATION_JSON_VALUE])fun metrics(): Flux<Metric> = repo.findAllReactive()// Newline-delimited JSON: one object per line, flushed incrementally@GetMapping("/metrics/stream", produces = [MediaType.APPLICATION_NDJSON_VALUE])fun stream(): Flux<Metric> = repo.findAllReactive()}
Streaming JSON: returning a Flux lets WebFlux serialize and flush each element as it is produced. With application/x-ndjson each item is an independent line, so huge exports never buffer fully in memory.
import kotlinx.coroutines.runBlockingdata class CreateUserRequest(val name: String, val email: String)// Stand-in for Jackson + the Kotlin module: required non-null fields must be presentfun decode(json: Map<String, String?>): CreateUserRequest {val name = json["name"] ?: error("missing required field: name")val email = json["email"] ?: error("missing required field: email")return CreateUserRequest(name, email)}fun main() = runBlocking {val ok = decode(mapOf("name" to "Ada", "email" to "ada@example.com"))println("decoded: " + ok)val result = runCatching { decode(mapOf("name" to "Ada")) }println("missing email -> " + result.exceptionOrNull()?.message)}
The Kotlin-module mental model, shown with plain coroutines and no Spring: nullability is part of the deserialization contract. This compiles and runs on kotlin stdlib + kotlinx.coroutines, simulating how a missing required field becomes an error.
Arena IDE🧠 Comprueba tu comprensión
0/1 · 0/1 answered1. In a coroutine WebFlux handler, why is awaitBody<CreateUserRequest>() preferred over manually parsing the request as a String?