Jackson ObjectMapper Basics with the Kotlin Module
One ObjectMapper plus jackson-module-kotlin turns your data classes into JSON and back, no boilerplate constructors required.
Jackson is the JSON engine that Spring Boot uses under the hood, and its central type is the ObjectMapper. An ObjectMapper is a configurable, thread-safe object that knows how to serialize Java/Kotlin objects into JSON text and deserialize JSON text back into objects. The two methods you will reach for constantly are writeValueAsString(value), which returns a JSON String, and readValue(json, Type), which parses a String (or stream) back into a typed instance. Because a configured mapper is thread-safe and relatively expensive to build, the idiomatic pattern is to create one instance and reuse it everywhere rather than constructing a fresh mapper per request.
Plain Jackson was designed for Java, and that creates friction with Kotlin. Out of the box, Jackson deserializes by calling a no-argument constructor and then setting fields one by one. Kotlin data classes typically have no no-arg constructor; they declare their properties as constructor parameters, often with val (immutable) and non-nullable types. If you point a vanilla ObjectMapper at a data class, you will hit errors like 'Cannot construct instance ... no Creators, like default constructor, exist', or you will silently lose Kotlin's null-safety guarantees because Jackson is happy to leave a non-null property null.
The fix is the jackson-module-kotlin dependency. Add it to your build (for example, com.fasterxml.jackson.module:jackson-module-kotlin on the classpath via Gradle) and register it on your mapper. This module teaches Jackson to read Kotlin's metadata: it can call the primary constructor with parameter names, it respects default parameter values when a JSON field is absent, and it honors nullability so that a missing or null JSON value for a non-nullable property fails fast with a MismatchedInputException (historically a MissingKotlinParameterException) instead of producing a corrupt object. In short, it makes idiomatic, immutable data classes 'just work' as your serialization model.
There are two common ways to wire it up. The terse, Kotlin-friendly way is the jacksonObjectMapper() factory function, which returns a ready-to-use ObjectMapper with the Kotlin module already registered. The explicit way is to start from a builder or a plain ObjectMapper and call registerKotlinModule() yourself: ObjectMapper().registerKotlinModule(). Both produce an equivalent result; jacksonObjectMapper() is simply a convenience wrapper. Use registerKotlinModule() when you are customizing a mapper you obtained from somewhere else and want to add Kotlin support to it.
Most real applications register more than one module. A typical setup chains registerKotlinModule() with JavaTimeModule (the jackson-datatype-jsr310 module) so that java.time types like Instant and LocalDate serialize as ISO-8601 strings instead of numeric timestamps. You will also frequently configure how unknown fields are handled: by default Jackson throws when it sees a JSON property that has no matching field, but calling configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) makes the mapper tolerant of extra fields, which is valuable when you consume third-party APIs that add properties over time.
A subtle but important detail in Kotlin is reified type information for generic targets. Jackson needs the full type, including type parameters, to deserialize collections and generics correctly; readValue(json, List::class.java) would erase the element type. The jackson-module-kotlin library ships an inline reified extension, readValue<T>(json), that captures the complete generic type for you. So mapper.readValue<List<User>>(jsonArray) round-trips a list of User objects with the element type preserved, which is exactly what you want and far cleaner than building a TypeReference by hand.
In a Spring Boot 3 application you usually do not create the mapper yourself for controllers and @RequestBody binding, because Spring Boot auto-configures a Jackson ObjectMapper bean and, when jackson-module-kotlin is on the classpath, registers the Kotlin module automatically. Your job is mainly to add the dependency and, if you need tweaks, expose a Jackson2ObjectMapperBuilderCustomizer or set spring.jackson.* properties. You still construct mappers explicitly for non-HTTP work: writing to Kafka, persisting JSON columns, calling external services with a WebClient body codec, or any place you serialize by hand.
Two practices keep Jackson code healthy. First, treat the ObjectMapper as a shared, immutable-after-configuration singleton; configure it once at startup and inject or reference that single instance. Second, model your JSON with explicit data classes and let nullability express optionality: a non-nullable property means the field is required, a nullable property (or one with a default) means it is optional. When you combine reusable mappers, the Kotlin module, and well-typed data classes, serialization becomes nearly invisible, boilerplate-free plumbing, and your domain types stay clean and idiomatic.
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.
🧠Check your understanding
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?