skip to content

How do you read RUNTIME annotations from functions, properties, and parameters using KClass members and KCallable in Kotlin reflection?

level: middleimportance: should knowfreq 45%

answer

  1. KCallable + KParameter are KAnnotatedElements
  2. declaredFunctions / declaredMemberProperties
  3. parameters: List<KParameter>, each has .annotations
  4. property.getter / backing field for use-site targets
  5. declared* = no inherited; member* = inherited too

basics

~10 s

Walk the class members: KClass exposes declaredFunctions and declaredMemberProperties, each a KCallable with its own .annotations. Function parameters are KParameter objects that also carry .annotations.

solid answer

~30 s

From a KClass you reach callables via members, declaredFunctions, declaredMemberProperties, declaredMemberFunctions, and constructors (from kotlin.reflect.full). Each is a KCallable, which is itself a KAnnotatedElement, so it has .annotations plus findAnnotation<T>()/hasAnnotation<T>(). For parameters, a KCallable exposes .parameters: List<KParameter>; each KParameter has .annotations too. Note a property's reflective annotations are those visible on the property element itself; if the source used a use-site target like @get:/@field:/@param:, the annotation lands on the getter (KProperty.getter), backing field, or constructor parameter respectively rather than the property's own .annotations list, so you may need to inspect those specific elements.

code

kotlin · 13 lines
kotlin
import kotlin.reflect.full.*

@Retention(AnnotationRetention.RUNTIME) annotation class Json(val name: String)

class Dto(@Json("user_id") val id: Long, val plain: String)

fun main() {
    val ctor = Dto::class.primaryConstructor!!
    ctor.parameters.forEach { p ->
        println("${p.name} -> ${p.findAnnotation<Json>()?.name}")
    }
    // id -> user_id ; plain -> null
}

go deeper

for a junior

Knows .annotations exists on members but may not enumerate functions/properties correctly.

for a middle

Enumerates declaredFunctions/declaredMemberProperties/parameters and reads each element's annotations.

for a senior

Handles use-site targets, checking getter/field/parameter, and knows declared vs inherited member APIs.

for a principal

Designs a resilient annotation-resolution strategy across elements and considers caching the reflective lookups.

## The element hierarchy Everything you can annotate is a `KAnnotatedElement`. Key sub-types: - `KClass<T>` — the class itself. - `KCallable<R>` — common parent of `KFunction` and `KProperty`. - `KParameter` — a single parameter of a callable. - `KProperty.getter` / `(KMutableProperty).setter` — accessors, themselves callables. All of these expose `.annotations: List<Annotation>` and work with `findAnnotation<T>()` / `hasAnnotation<T>()`. ## Enumerating members From `kotlin.reflect.full`: ```kotlin import kotlin.reflect.full.* val k = MyClass::class k.declaredFunctions // functions declared directly k.declaredMemberProperties // properties declared directly k.declaredMemberFunctions k.memberProperties // includes inherited k.constructors ``` `declared*` exclude inherited members; the non-`declared` variants include inherited ones. ## Reading per-element annotations ```kotlin @Retention(AnnotationRetention.RUNTIME) annotation class Json(val name: String) @Retention(AnnotationRetention.RUNTIME) annotation class Validated class Dto(@Json("user_id") val id: Long) { @Validated fun check() {} } val props = Dto::class.declaredMemberProperties val idAnno = props.first { it.name == "id" }.findAnnotation<Json>() val fnHasValidated = Dto::class.declaredFunctions.first().hasAnnotation<Validated>() ``` ## Parameters ```kotlin val ctor = Dto::class.primaryConstructor!! for (p: KParameter in ctor.parameters) { val j = p.findAnnotation<Json>() println("${p.name} -> ${j?.name}") } ``` Each `KParameter` is annotated independently of the corresponding property. ## Use-site targets matter When the source declares `@get:Json` or `@field:Json` or `@param:Json`, the annotation does not sit on the property element — it lands on the getter, backing field, or constructor parameter. So `property.findAnnotation<Json>()` may return null while `property.getter.findAnnotation<Json>()` returns it. A robust reader checks the relevant target element (e.g., property, getter, and constructor parameter) rather than only the property's own `.annotations`. This is exactly why serialization libraries inspect multiple elements.

  • Why might declaredMemberProperties.findAnnotation<Json>() return null when the field clearly has @field:Json?
    The use-site target @field: places it on the backing field, not the property element; you must inspect the field (e.g., via the Java field) or use the right target element.
  • What is the difference between members and declaredMembers?
    members includes inherited callables from supertypes; declaredMembers only those declared directly on the class.

saying these in an interview costs you the question

  • Assuming a property's .annotations always includes annotations declared with @get:/@field:/@param:.
  • Not knowing KParameter carries its own annotations.
  • Confusing declaredFunctions (own) with functions/members (inherited included).
  • Thinking you must go through Java reflection to read parameter annotations in Kotlin.
  • Believing constructor parameters and properties share one annotation list.

context