Anotaciones de Jackson en Kotlin: Modelando tu JSON
Domina las anotaciones esenciales de Jackson y el detalle de los use-site targets en Kotlin que rompe tu serializacion en silencio.
Jackson es el motor de JSON que Spring Boot configura por defecto, y la mayor parte del tiempo simplemente funciona: una data class de Kotlin se convierte en un objeto JSON y vuelve sin ninguna configuracion. Pero las APIs reales rara vez coinciden a la perfeccion con tu modelo de dominio. El formato de transporte usa snake_case mientras tu codigo usa camelCase, algunos campos nunca deben salir del servidor, y los payloads externos envian el mismo valor bajo tres nombres distintos segun la version de la API. Las anotaciones de Jackson son la forma de salvar esa brecha de manera declarativa, manteniendo limpias tus data classes mientras controlas exactamente como se ve el JSON.
El caballo de batalla es @JsonProperty, que renombra un campo. Si el cliente envia "first_name" pero tu propiedad es firstName, anotarla con @JsonProperty("first_name") mapea ambos. La misma anotacion controla el nombre de salida durante la serializacion, asi que una sola declaracion cubre ambas direcciones. Tambien puedes marcar una propiedad como required = true para que Jackson falle si falta en la entrada, lo cual es util para una validacion fail-fast en la frontera de deserializacion antes de que se ejecute tu logica de negocio.
@JsonIgnore elimina una propiedad del JSON por completo. El uso clasico es un hash de contrasena o una bandera interna de auditoria que jamas debe serializarse en una respuesta. Ten cuidado: @JsonIgnore es bidireccional por defecto, asi que un campo ignorado tambien se omite durante la deserializacion. Si quieres un campo de solo escritura (aceptado en la entrada, oculto en la salida) o de solo lectura, usa @JsonProperty(access = Access.WRITE_ONLY) o READ_ONLY, que dan control por direccion que un @JsonIgnore contundente no puede ofrecer.
Cuando el mismo valor logico llega bajo claves distintas, @JsonAlias acepta una lista de nombres alternativos durante la deserializacion mientras mantiene un unico nombre de salida canonico definido por @JsonProperty. Esto es perfecto para APIs versionadas: @JsonProperty("userId") @JsonAlias("user_id", "uid") lee cualquiera de los tres pero siempre escribe "userId". Para construir objetos, @JsonCreator le indica a Jackson que constructor o metodo de fabrica usar; con las data classes de Kotlin el constructor primario suele detectarse automaticamente al registrar el modulo de Kotlin, pero @JsonCreator se vuelve esencial para funciones de fabrica personalizadas, enums parseados desde un string o value classes que envuelven un solo campo.
@JsonInclude(JsonInclude.Include.NON_NULL) recorta los campos nulos de la salida para que tus respuestas sean mas ligeras y tus clientes no tengan que manejar nulos explicitos. Puedes aplicarlo a una sola propiedad, a una clase entera o de forma global mediante la configuracion del ObjectMapper. Valores relacionados como NON_EMPTY (descarta strings y colecciones vacias) y NON_ABSENT (entiende los nullables de Kotlin y Optional) te permiten afinar que cuenta como omisible. Esto encaja de forma natural con los tipos nullable de Kotlin: un String? que es null simplemente desaparece del JSON.
Ahora el detalle critico de Kotlin. Cuando anotas una propiedad en una clase de Kotlin, la anotacion podria recaer legitimamente en varios lugares: el campo de respaldo, el getter, el setter, el parametro del constructor u otros. Kotlin los llama use-site targets, escritos como @field:, @get:, @param:, etc. Si escribes @JsonProperty("name") sin target, Kotlin aplica su precedencia por defecto (param, luego property, luego field), y la mayoria de las veces Jackson la encuentra. Pero en cuanto mezclas getters personalizados, propiedades calculadas o interoperabilidad con accesores al estilo Java, una anotacion sin target puede adherirse al elemento equivocado y Jackson la ignora en silencio, produciendo el nombre de campo sin renombrar y sin ningun error.
La regla segura: se explicito. Usa @field:JsonProperty("first_name") cuando quieras la anotacion en el campo subyacente, que es la opcion mas confiable para propiedades simples de data class porque el modulo de Kotlin de Jackson lee la metadata del campo. Usa @get:JsonProperty("fullName") para propiedades calculadas o derivadas que tienen getter pero no un campo real de respaldo, como val fullName get() = first + " " + last, ya que ahi no hay campo que anotar. Confundir esto es la causa numero uno de los misteriosos bugs de "mi @JsonProperty no hace nada" en proyectos de Kotlin con Spring.
En la practica, registra el modulo de Kotlin de Jackson (jackson-module-kotlin, que Spring Boot incluye automaticamente cuando detecta Kotlin) para que se entienda el constructor de la data class, prefiere targets @field: para propiedades declaradas y @get: para las calculadas, y apoyate en @JsonInclude para payloads limpios. Con estas pocas anotaciones aplicadas con intencion, obtienes contratos JSON precisos y tolerantes a versiones sin contaminar tu modelo de dominio ni escribir un solo serializador personalizado.
import com.fasterxml.jackson.annotation.*@JsonInclude(JsonInclude.Include.NON_NULL)data class UserDto(// Reads "user_id", but also accepts the legacy "uid" from older clients@field:JsonProperty("user_id")@field:JsonAlias("uid", "id")val userId: Long,@field:JsonProperty("first_name")val firstName: String,// Accepted on input, never serialized back out@field:JsonProperty(access = JsonProperty.Access.WRITE_ONLY)val password: String,// Internal flag: completely hidden from JSON, both directions@field:JsonIgnoreval internalScore: Int = 0,// Null disappears from output thanks to @JsonInclude(NON_NULL)val nickname: String? = null,)
Core annotations on a data class: rename, ignore, alias, and trim nulls. Note the explicit @field: use-site targets so Jackson reliably picks them up.
import com.fasterxml.jackson.annotation.JsonPropertydata class Person(@field:JsonProperty("first_name")val first: String,@field:JsonProperty("last_name")val last: String,) {// No backing field exists here, so @field: would attach to nothing.// Use @get: so Jackson sees the getter that produces this value.@get:JsonProperty("full_name")val fullName: Stringget() = "$first $last"}// Serializes to: {"first_name":"Ada","last_name":"Lovelace","full_name":"Ada Lovelace"}
The Kotlin gotcha: @field: for backing fields vs @get: for computed properties. Annotating the wrong target makes Jackson silently ignore your rename.
import com.fasterxml.jackson.annotation.JsonCreatorimport com.fasterxml.jackson.annotation.JsonValueimport com.fasterxml.jackson.databind.ObjectMapperimport com.fasterxml.jackson.module.kotlin.registerKotlinModule@JvmInlinevalue class Email private constructor(@get:JsonValue val raw: String) {companion object {@JsonCreator@JvmStaticfun of(value: String): Email {require("@" in value) { "Invalid email: $value" }return Email(value.lowercase())}}}data class Account(val email: Email)val mapper = ObjectMapper().registerKotlinModule()// reads {"email":"Ada@Example.com"} -> Account(email=Email(ada@example.com))
@JsonCreator with a factory function, ideal for value classes or enums parsed from a single scalar. Register the Kotlin module so constructors are understood.
🧠 Comprueba tu comprensión
0/1 · 0/1 answered1. In a Kotlin data class, you add @JsonProperty("full_name") (with no use-site target) to a computed property that has only a custom getter and no backing field. Jackson ignores the rename. What is the most likely fix?