skip to content

You annotate a Kotlin data class property with a Jackson/JPA annotation and it seems ignored at runtime. How do use-site targets explain and fix this?

level: middleimportance: should knowfreq 60%

answer

  1. Wrong element ⇒ silently ignored, no compile error
  2. Jackson serialization → @get:
  3. JPA field access → @field: (keep @Id/@Column there)
  4. Bean Validation on bean → @field:/@get:, not @param:
  5. Be explicit on interop boundaries

basics

~20 s

The annotation probably landed on the wrong JVM element. The library reads, say, the getter, but Kotlin put your annotation on the field or the constructor parameter. Adding the right prefix like @get: or @field: makes it visible.

solid answer

~40 s

Libraries inspect a specific JVM element reflectively: Jackson typically resolves both fields and getters but precedence and visibility depend on config; JPA/Hibernate uses field-access or property(getter)-access depending on where `@Id` is placed; Bean Validation can check field, getter, or constructor parameter. When you write `@JsonProperty("name")` on a primary-constructor `val`, the default resolution sends it to `param` (the constructor parameter), which Jackson's serialization path may not read for output, so it appears ignored. The fix is to be explicit: `@get:JsonProperty("name")` for serialization, `@field:Column(...)` / `@field:Id` for field-access JPA. For Bean Validation on constructor params you often want `@field:` or `@get:` so the constraint is enforced on read, not only on construction. Rule of thumb: **on JVM-interop boundaries, always state the target.**

code

kotlin · 9 lines
kotlin
data class ProductDto(
    @get:JsonProperty("product_id")
    @field:NotNull
    val productId: Long,

    @get:JsonProperty("name")
    @get:NotBlank
    val name: String,
)

go deeper

for a junior

Recognizes that the annotation may be on the wrong element and tries adding a prefix.

for a middle

Maps each library (Jackson/JPA/Validation) to the element it reads and chooses the right prefix.

for a senior

Explains access-type consistency (e.g. @Id placement) and why there's no compile error.

for a principal

Defines a team rule for interop annotations and code-review checks to keep targets consistent.

## Why "it's ignored" happens A Kotlin primary-constructor property `data class User(val name: String)` generates a parameter, a field, and a getter. Reflection-based libraries read a **particular** element: - **Jackson** discovers properties from getters/setters and fields; which one wins depends on visibility and `MapperFeature` config. The Kotlin module helps, but an annotation on the wrong element can still be skipped. - **JPA/Hibernate** uses **field access** or **property (getter) access**. The access type is usually decided by where you put `@Id` — `@Id` on the field ⇒ field access; on the getter ⇒ property access. With Kotlin, an unprefixed `@Id` on a ctor property defaults toward `param`/`field`, which can mismatch your intended access type. - **Bean Validation (jakarta.validation)** can validate the field, the getter, or the constructor parameter. `@NotBlank` on a ctor `val` defaults to `param`, so it's only checked when the validator processes constructor parameters — not on a plain `validate(bean)` field/getter pass. ## The fix: be explicit ```kotlin data class UserDto( @get:JsonProperty("user_name") // serialization reads the getter @get:NotBlank // validate via the getter on validate(bean) val name: String, ) @Entity class UserEntity( @field:Id // field-access JPA @field:Column(name = "id") val id: Long, ) ``` ## Decision guide - **Jackson serialization/deserialization** → usually `@get:` (and `@set:`/`@field:` as needed). The Jackson Kotlin module mitigates a lot, but explicit `@get:`/`@field:` removes ambiguity. - **JPA field access** → `@field:`. Keep `@Id`, `@Column`, `@OneToMany` consistently on the field. - **Bean Validation enforced on the bean** → `@field:` or `@get:`; use `@param:` only when you specifically validate constructor parameters. ## Why no compile error warns you The default resolution is always *valid* — the annotation lands somewhere legal — so the compiler is silent. The bug only surfaces at runtime when the library doesn't find the annotation where it looked. That's why the **explicit-target rule** on interop boundaries prevents an entire class of silent failures.

  • Why is there no compiler warning when the annotation goes to the 'wrong' place?
    Because the default-resolved target is always a legal one for that annotation; nothing is syntactically wrong. The mismatch is purely a runtime expectation of the consuming library.
  • How does @Id placement affect Hibernate access type?
    @Id on the field selects field access; on the getter it selects property access. In Kotlin you control this with @field:Id vs @get:Id, and all other mappings must match that access type.

The annotation is a sticky note; the library only ever reads one drawer. Put the note in the drawer it actually opens.

saying these in an interview costs you the question

  • Blaming the library instead of checking the use-site target
  • Adding the Jackson Kotlin module and assuming targets no longer matter
  • Putting @Id on field but @Column on getter, mixing access types
  • Expecting @param:NotBlank to be enforced by validate(bean)

context