skip to content

Explain Kotlin's default use-site target resolution. Given an annotation valid on multiple targets, how does the compiler choose, and how do `@param:`, `@property:`, and `@field:` differ in visibility to Java reflection?

level: seniorimportance: should knowfreq 30%

answer

  1. Default order: param > property > field, first match only
  2. @field: + @param: visible to Java reflection
  3. @property: = Kotlin metadata, invisible to plain Java
  4. Single-@Target annotations skip the ambiguity
  5. Explicit target = safe at the boundary

basics

~20 s

If you omit the target, the compiler picks the first applicable one from param, then property, then field. @param: and @field: are real Java elements reflection can see; @property: is Kotlin-only metadata that plain Java can't read.

solid answer

~40 s

When no use-site target is given, Kotlin resolves it against the annotation's `@Target` set using the fixed priority `param > property > field`, choosing the **first** applicable element only. `@param:` annotates the constructor parameter (visible via constructor reflection), `@field:` annotates the backing field (visible via field reflection — what JPA/Jackson use), and `@property:` is stored in the Kotlin `@Metadata` and is **invisible** to ordinary Java reflection (only Kotlin reflection / `kotlin-reflect` sees it). So an annotation that defaults to `property` (because it isn't applicable on `param`/`field`) will appear to vanish from Java's point of view. Annotations restricted to a single `@Target` (e.g. only fields) skip the ambiguity. This explains why bare annotations sometimes work, sometimes silently no-op, and why explicit targets are the safe choice at the Java boundary.

go deeper

for a junior

Can name the targets but may not recall the exact default order or Java visibility differences.

for a middle

States the param>property>field order and that only one element is chosen.

for a senior

Explains how @Target intersects with emitted elements and the Java-reflection visibility of each target, predicting silent no-ops.

for a principal

Reasons about library API stability and reflection contracts across frameworks, mandating explicit targets in public/persistence code.

## The resolution algorithm For a property declaration, the compiler determines the set of *applicable* use-site targets by intersecting: - the elements the property actually emits (param only if it's a primary-constructor property; setter only for `var`), with - the annotation's own `@Target(...)` meta-annotation (which Kotlin `AnnotationTarget`s it allows). If you wrote an explicit target, that target is used (and must be applicable, or you get an error). If you wrote **no** target, the compiler walks this fixed priority list and picks the **first** applicable entry: 1. `param` 2. `property` 3. `field` Only **one** element is annotated — not all applicable ones. ```kotlin class C(@Ann val x: Int) // If @Ann allows param + field: it lands on PARAM (param wins). // If @Ann allows only field: it lands on FIELD. // If @Ann allows only property: it lands on PROPERTY (Kotlin metadata only). ``` ## Java visibility of each target - **`@param:`** -> annotation on the **constructor parameter**. Visible via `Constructor.getParameterAnnotations()`. Most ORMs/serializers do *not* scan constructor params, so this is a common silent miss. - **`@field:`** -> annotation on the **backing field**. Visible via `Field.getAnnotations()`. This is what JPA field access, Jackson, and Hibernate Validator typically read. - **`@get:` / `@set:`** -> annotation on the **getter/setter method**. Visible via `Method.getAnnotations()`. Used by property/getter-access frameworks. - **`@property:`** -> stored in the class's **Kotlin `@Metadata`** blob, *not* on any Java element. **Invisible to plain Java reflection**; only readable via `kotlin-reflect`. - **`@receiver:`** -> annotates the receiver parameter of an extension; affects the extension's first (receiver) parameter. - **`@setparam:`** / **`@delegate:`** -> setter's value parameter / the delegate-storage field (`by`). ## Why this trips people up Because the default is the *first* applicable target, a bare annotation can: - land on `param` (invisible to a field-scanning ORM), or - land on `property` (invisible to all of Java), producing a runtime no-op with **no compile error**. Conversely, an annotation that is `@Target(FIELD)`-only has exactly one applicable target and "just works" without a prefix. ## Practical guidance - At any Java/framework boundary, write the target **explicitly** (`@field:`, `@get:`) rather than relying on defaults. - Reserve `@property:` for annotations you only consume via Kotlin reflection. - Remember setter-only and param-only targets don't exist for `val`/non-constructor properties; choosing them is a compile error.

  • Why might an annotation 'disappear' entirely from Java even though it compiled fine?
    It defaulted to `property` (because it wasn't applicable on param/field), so it lives only in Kotlin @Metadata and plain Java reflection can't see it.
  • When does the default resolution NOT include `param`?
    When the property isn't declared in the primary constructor (no parameter exists) or the annotation's @Target doesn't allow VALUE_PARAMETER.

saying these in an interview costs you the question

  • Saying all applicable targets get the annotation
  • Claiming the default is field-first
  • Thinking @property: is visible to Java reflection
  • Not knowing param-targeted annotations are missed by field-scanning frameworks

context