Phase 5 · Advanced KotlinModule 27~32 min read

Annotations & Reflection

Add metadata with annotations and inspect code at runtime with reflection.

What you'll learn

Annotations attach metadata to code, and reflection lets a program inspect itself at runtime. Together they power the frameworks you'll use every day — test runners, JSON libraries, dependency injection.

By the end you'll be able to:

  • Use built-in annotations and create your own
  • Control annotations with @Target and @Retention
  • Inspect types at runtime with reflection (::class)
  • Understand how frameworks combine the two

Annotations

An annotation is metadata — a note on your code that the compiler, tools, or a framework can read. You've seen a few already (@Override isn't needed in Kotlin, but @Deprecated, @JvmStatic, and @Test are common):

Builtin.kt
open class Base {
    open fun greet() = "hi"
}

class Child : Base() {
    override fun greet() = "hey"

    @Deprecated("Use greet() instead")   // warns callers this is obsolete
    fun oldGreet() = "hello"
}

println(Child().greet())

Custom annotations

You define your own with the annotation class keyword. Two meta-annotations configure it: @Target says where it can be applied (functions, classes, properties…), and @Retention says how long it survives — crucially, RUNTIME is required for it to be visible to reflection:

Custom.kt
@Target(AnnotationTarget.FUNCTION)      // may annotate functions
@Retention(AnnotationRetention.RUNTIME) // keep it at runtime for reflection
annotation class Test

class MathTests {
    @Test fun addsCorrectly() { }
    @Test fun subtractsCorrectly() { }
}

Note

For an annotation to be readable by a framework at runtime, it must be @Retention(AnnotationRetention.RUNTIME). That's how a test runner finds every function marked @Test and runs it.

Reflection

Reflection lets code examine itself at runtime — a type's name, its members, its annotations. Every value gives you its KClass via ::class, the gateway to this information:

Reflection.kt
val text = "Hello"
println(text::class)                // class kotlin.String
println(text::class.simpleName)     // String

class Person(val name: String, val age: Int)
val p = Person("Sara", 25)
println(p::class.simpleName)        // Person

Annotations + reflection is the recipe behind almost every Kotlin/JVM framework:

The framework pattern

You add annotations

@Test, @Serializable…

→

The framework scans

with reflection at startup

→

It acts on them

runs tests, maps JSON…

Use it sparingly

Reflection is powerful but comes at a cost: it's slower than direct calls, bypasses compile-time safety, and needs the extra kotlin-reflect library for full features. Reach for it to build tools and frameworks — not for everyday logic a normal call would handle.

Recap & quick check

Key takeaways

  • Annotations attach metadata; built-ins include @Deprecated and @JvmStatic (@Override isn't needed).
  • Create your own with 'annotation class'; configure with @Target and @Retention.
  • @Retention(RUNTIME) is required for an annotation to be visible to reflection.
  • Reflection inspects types at runtime via ::class (a KClass).
  • Frameworks combine annotations + reflection; use reflection sparingly (slow, unsafe, needs kotlin-reflect).

Quick check

1. What are annotations?

2. What keyword defines a custom annotation?

3. For an annotation to be readable at runtime by reflection, its retention must be…

4. How do you get a value's type information at runtime?

5. How do frameworks like test runners work under the hood?

Great — you've seen the machinery behind Kotlin's frameworks. Next up: Module 28 — Domain-Specific Languages (DSLs).