Fundamentos de Jackson ObjectMapper con el módulo de Kotlin
Un ObjectMapper más jackson-module-kotlin convierte tus data classes en JSON y de vuelta, sin constructores repetitivos.
Jackson es el motor de JSON que Spring Boot usa internamente, y su tipo central es el ObjectMapper. Un ObjectMapper es un objeto configurable y seguro para hilos que sabe serializar objetos de Java/Kotlin en texto JSON y deserializar texto JSON de vuelta en objetos. Los dos métodos que usarás constantemente son writeValueAsString(value), que devuelve un String JSON, y readValue(json, Tipo), que parsea un String (o un stream) de vuelta en una instancia tipada. Como un mapper configurado es seguro para hilos y relativamente costoso de construir, el patrón idiomático es crear una sola instancia y reutilizarla en todas partes en lugar de construir un mapper nuevo por petición.
Jackson puro fue diseñado para Java, y eso genera fricción con Kotlin. De fábrica, Jackson deserializa llamando a un constructor sin argumentos y luego asignando los campos uno por uno. Las data classes de Kotlin normalmente no tienen un constructor sin argumentos; declaran sus propiedades como parámetros del constructor, a menudo con val (inmutables) y tipos no nulos. Si apuntas un ObjectMapper estándar a una data class, te toparás con errores como 'Cannot construct instance ... no Creators, like default constructor, exist', o perderás silenciosamente las garantías de null-safety de Kotlin porque a Jackson no le importa dejar en null una propiedad no nula.
La solución es la dependencia jackson-module-kotlin. Agrégala a tu build (por ejemplo, com.fasterxml.jackson.module:jackson-module-kotlin en el classpath vía Gradle) y regístrala en tu mapper. Este módulo le enseña a Jackson a leer los metadatos de Kotlin: puede llamar al constructor primario usando los nombres de los parámetros, respeta los valores por defecto de los parámetros cuando un campo JSON está ausente, y honra la nulabilidad de modo que un valor JSON faltante o null para una propiedad no nula falla de inmediato con una MismatchedInputException (históricamente una MissingKotlinParameterException) en lugar de producir un objeto corrupto. En resumen, hace que las data classes idiomáticas e inmutables 'simplemente funcionen' como tu modelo de serialización.
Hay dos formas comunes de conectarlo. La forma concisa y amigable con Kotlin es la función fábrica jacksonObjectMapper(), que devuelve un ObjectMapper listo para usar con el módulo de Kotlin ya registrado. La forma explícita es partir de un builder o de un ObjectMapper plano y llamar tú mismo a registerKotlinModule(): ObjectMapper().registerKotlinModule(). Ambas producen un resultado equivalente; jacksonObjectMapper() es simplemente un envoltorio de conveniencia. Usa registerKotlinModule() cuando estés personalizando un mapper que obtuviste de otro lugar y quieras agregarle soporte para Kotlin.
La mayoría de las aplicaciones reales registran más de un módulo. Una configuración típica encadena registerKotlinModule() con JavaTimeModule (el módulo jackson-datatype-jsr310) para que los tipos de java.time como Instant y LocalDate se serialicen como cadenas ISO-8601 en lugar de timestamps numéricos. También configurarás con frecuencia cómo se manejan los campos desconocidos: por defecto Jackson lanza una excepción cuando ve una propiedad JSON sin un campo correspondiente, pero llamar a configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) hace que el mapper tolere campos extra, lo cual es valioso cuando consumes APIs de terceros que agregan propiedades con el tiempo.
Un detalle sutil pero importante en Kotlin es la información de tipo reificada para objetivos genéricos. Jackson necesita el tipo completo, incluidos los parámetros de tipo, para deserializar colecciones y genéricos correctamente; readValue(json, List::class.java) borraría el tipo del elemento. La librería jackson-module-kotlin incluye una extensión inline reificada, readValue<T>(json), que captura el tipo genérico completo por ti. Así, mapper.readValue<List<User>>(jsonArray) reconstruye una lista de objetos User preservando el tipo del elemento, que es exactamente lo que quieres y mucho más limpio que construir un TypeReference a mano.
En una aplicación Spring Boot 3 normalmente no creas el mapper tú mismo para los controladores ni para el binding de @RequestBody, porque Spring Boot autoconfigura un bean ObjectMapper de Jackson y, cuando jackson-module-kotlin está en el classpath, registra el módulo de Kotlin automáticamente. Tu tarea es principalmente agregar la dependencia y, si necesitas ajustes, exponer un Jackson2ObjectMapperBuilderCustomizer o definir propiedades spring.jackson.*. Aun así construyes mappers explícitamente para trabajo que no es HTTP: escribir en Kafka, persistir columnas JSON, llamar a servicios externos con un codec de cuerpo de WebClient, o cualquier lugar donde serialices a mano.
Dos prácticas mantienen sano el código con Jackson. Primero, trata el ObjectMapper como un singleton compartido e inmutable tras su configuración; configúralo una vez al arranque e inyecta o referencia esa única instancia. Segundo, modela tu JSON con data classes explícitas y deja que la nulabilidad exprese la opcionalidad: una propiedad no nula significa que el campo es obligatorio, una propiedad nula (o con un valor por defecto) significa que es opcional. Cuando combinas mappers reutilizables, el módulo de Kotlin y data classes bien tipadas, la serialización se vuelve una fontanería casi invisible y sin boilerplate, y tus tipos de dominio se mantienen limpios e idiomáticos.
import com.fasterxml.jackson.databind.ObjectMapperimport com.fasterxml.jackson.module.kotlin.registerKotlinModuleimport com.fasterxml.jackson.module.kotlin.readValuedata class User(val id: Long, val name: String, val active: Boolean = true)// Configure once, reuse everywhere (thread-safe after setup).val mapper: ObjectMapper = ObjectMapper().registerKotlinModule()fun main() {val user = User(id = 1, name = "Ada")// Serialize: object -> JSON Stringval json = mapper.writeValueAsString(user)println(json) // {"id":1,"name":"Ada","active":true}// Deserialize: JSON String -> typed objectval parsed: User = mapper.readValue(json)println(parsed) // User(id=1, name=Ada, active=true)}
Build a reusable ObjectMapper for Kotlin and round-trip a data class. registerKotlinModule() lets Jackson use the primary constructor of an immutable data class with no no-arg constructor.
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapperimport com.fasterxml.jackson.module.kotlin.readValuedata class Product(val sku: String, val price: Double)val mapper = jacksonObjectMapper()fun main() {val jsonArray = """[{"sku":"A1","price":9.99},{"sku":"B2","price":19.5}]"""// Without reified types, the element type would be erased.val products: List<Product> = mapper.readValue(jsonArray)println(products.size) // 2println(products.first().sku) // A1}
jacksonObjectMapper() is the concise factory (Kotlin module already registered). The reified readValue<T>() captures full generic types, so List element types survive deserialization.
import com.fasterxml.jackson.databind.DeserializationFeatureimport com.fasterxml.jackson.databind.SerializationFeatureimport com.fasterxml.jackson.databind.json.JsonMapperimport com.fasterxml.jackson.datatype.jsr310.JavaTimeModuleimport com.fasterxml.jackson.module.kotlin.kotlinModuleval mapper = JsonMapper.builder().addModule(kotlinModule()) // Kotlin data-class support.addModule(JavaTimeModule()) // Instant/LocalDate as ISO-8601, not epoch numbers.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS).configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build()
A production-style mapper: Kotlin support, java.time as ISO-8601 strings, and tolerance for unknown fields from evolving external APIs.
import com.fasterxml.jackson.databind.DeserializationFeatureimport org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizerimport org.springframework.context.annotation.Beanimport org.springframework.context.annotation.Configurationimport org.springframework.web.bind.annotation.*data class CreateUserRequest(val name: String, val email: String)@RestController@RequestMapping("/users")class UserController {@PostMappingfun create(@RequestBody body: CreateUserRequest): String ="Created " + body.name // Spring deserializes JSON into the data class for you}@Configurationclass JacksonConfig {@Beanfun kotlinCustomizer() = Jackson2ObjectMapperBuilderCustomizer { b ->b.featuresToDisable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)}}
In Spring Boot 3 you rarely build the mapper for controllers; with jackson-module-kotlin on the classpath, @RequestBody binds directly into a data class. Customize the auto-configured mapper via a customizer bean.
🧠 Comprueba tu comprensión
0/1 · 0/1 answered1. Why does a plain ObjectMapper without jackson-module-kotlin often fail to deserialize JSON into an immutable Kotlin data class?