skip to content

You are designing a validation annotation that a runtime framework will read on data-class properties via reflection. What @Retention and @Target would you choose, and why does the property/field/getter distinction matter?

level: seniorimportance: should knowfreq 33%

answer

  1. Runtime read → RUNTIME retention, no exceptions
  2. One property = property + field + getter + (param) sites
  3. Annotation lands on ONE default site, not all
  4. Mismatch = silent no-op
  5. Fix: broaden @Target or require @field:/@get:

basics

~20 s

Use RUNTIME retention so reflection can read it, and target the property (and maybe field/parameter). Because a property, its field, and its getter are separate places, you must make sure the annotation lands where your framework actually looks.

solid answer

~40 s

Choose @Retention(AnnotationRetention.RUNTIME) so the validator can read it via reflection (KProperty.annotations / findAnnotation / Java getAnnotation). For @Target, decide where the framework inspects: if it iterates KClass.memberProperties, target PROPERTY; if it reads JVM fields via Java reflection, you also need FIELD plus a @field: use-site target. For data classes whose properties come from the primary constructor, also consider VALUE_PARAMETER, since the annotation defaults to the constructor parameter there unless redirected. The property/field/getter split matters because annotating a property does not propagate to its backing field or accessor — Kotlin emits it to a default site (property/parameter), and if your framework reads the field it will see nothing. Document the required use-site target, or broaden @Target to cover all sites the framework may inspect.

code

kotlin · 9 lines
kotlin
@Target(
    AnnotationTarget.PROPERTY,
    AnnotationTarget.FIELD,
    AnnotationTarget.VALUE_PARAMETER,
)
@Retention(AnnotationRetention.RUNTIME)
annotation class NotBlank

data class User(@field:NotBlank val email: String)

go deeper

for a junior

Knows it must be RUNTIME to be read by reflection but may not foresee the target/site mismatch.

for a middle

Picks RUNTIME and an appropriate target, and knows @field:/@get: exist.

for a senior

Anticipates the silent-no-op from the property/field/getter/parameter split and broadens @Target or mandates a use-site target accordingly.

for a principal

Designs the annotation contract holistically — read-site, default-site precedence, documentation, and consumer ergonomics — to prevent misuse at scale.

## The design question A runtime validation framework (think Bean Validation / your own) reads annotations off properties via reflection and runs checks. Two meta-annotation decisions drive correctness. ## Retention: must be RUNTIME Reflection only sees **RUNTIME**-retained annotations. SOURCE/BINARY would make `findAnnotation`, `KProperty.annotations`, or Java `getAnnotation` return nothing. So: ```kotlin @Retention(AnnotationRetention.RUNTIME) annotation class NotBlank ``` This is also the default, but stating it documents intent. ## Target: align with where the framework reads Kotlin generates **multiple** annotatable elements from one property: - the **property** (`PROPERTY`), - the **backing field** (`FIELD`), - the **getter** (`PROPERTY_GETTER`) / setter, - for primary-constructor properties, the **constructor parameter** (`VALUE_PARAMETER`). When you write `@NotBlank val name: String`, Kotlin applies the annotation to a **single default site**, chosen by precedence (roughly: constructor parameter → property → field, depending on what @Target allows). It does **not** copy the annotation onto all of them. ### Why this bites you If your framework reads the **JVM field** (common when using Java reflection or libraries that scan `Field`), but the annotation landed on the **property** or **parameter**, the field has no annotation and validation silently does nothing. ### Fixes 1. **Match @Target to the read site.** If the framework scans Kotlin properties via `KClass.memberProperties`, target `PROPERTY` and read with `prop.findAnnotation<NotBlank>()`. 2. **Use a use-site target.** Require callers to write `@field:NotBlank` / `@get:NotBlank` so it lands where you read. 3. **Broaden @Target** to include every site you might inspect, e.g.: ```kotlin @Target( AnnotationTarget.PROPERTY, AnnotationTarget.FIELD, AnnotationTarget.VALUE_PARAMETER, ) @Retention(AnnotationRetention.RUNTIME) annotation class NotBlank ``` ## Reading it reflectively ```kotlin import kotlin.reflect.full.findAnnotation import kotlin.reflect.full.memberProperties data class User(val name: String, @field:NotBlank val email: String) fun validate(obj: Any) { obj::class.memberProperties.forEach { prop -> if (prop.findAnnotation<NotBlank>() != null) { /* check value */ } } } ``` Note `KProperty.findAnnotation` finds annotations on the property element; if you scanned Java `Field`s instead you'd need the FIELD site. ## Summary of choices - **Retention:** RUNTIME (non-negotiable for reflective reading). - **Target:** the precise site(s) your reader inspects; broaden or document a use-site target to avoid the silent-no-op trap.

  • Your framework scans Java Fields but users write @NotBlank val x; why does nothing get validated?
    The annotation defaulted to the property/parameter site, not the backing field, so Field.getAnnotation returns null. Require @field: or include FIELD in @Target plus default-site handling.
  • For a primary-constructor property, where does an unqualified annotation go by default?
    If VALUE_PARAMETER is an allowed target it goes to the constructor parameter first; otherwise to the property, then field, per Kotlin's use-site precedence.

A property is a building with several doors (field, getter, parameter). Your label hangs on one door; if the inspector checks a different door, they find nothing.

saying these in an interview costs you the question

  • Choosing SOURCE or BINARY for something read at runtime
  • Assuming annotating a property covers field and getter automatically
  • Not anticipating the silent-no-op when target and read-site mismatch
  • Ignoring constructor-parameter default site for data classes
  • Not documenting the required use-site target for consumers

context