Reflection & Annotations
🔬 Reflection & Annotations
KClass reflection API, writing custom annotations, and the use-site targets Kotlin needs because a single property can compile into several different underlying elements.
🏷️ KClass — Kotlin's Reflection Entry Point
Every type has a corresponding KClass object, accessed with ::class, which describes that type's structure:
Type::class works on a type name directly; instance::class works on a value, returning its actual runtime class — useful when a value is typed more generally than its concrete class (e.g. a variable typed Animal holding a Dog, as in Course 1's inheritance chapter).
Basic ::class and simple properties like simpleName work out of the box, but deeper reflection — listing a class's properties and functions, calling a function dynamically — requires the kotlin-reflect library as an explicit Gradle dependency. It's deliberately not bundled by default, since full reflection support adds real size to a compiled app, and most Kotlin code (especially on Android) never needs it.
🔎 Inspecting Members
With kotlin-reflect available, a KClass can list its own properties and functions:
prop.get(p) reads that property's value off a specific instance, generically — the code above works for any data class passed in, without knowing its properties in advance. This generic, "works on any type" capability is exactly what makes reflection useful for building libraries: a JSON serializer, for instance, can walk any object's properties this way and turn them into JSON fields without the library author needing to know about Person specifically.
🏷️ Custom Annotations
An annotation is declared with the annotation keyword, and can be applied to classes, properties, functions, or parameters:
By itself, an annotation is inert — it's just metadata attached to a declaration. It becomes useful once something reads it back, which is exactly what reflection is for:
@Retention — How Long It Survives
@Retention(AnnotationRetention.RUNTIME) keeps an annotation available for reflection at runtime (the default). SOURCE discards it after compiling; BINARY keeps it in the compiled file but not visible to runtime reflection.
@Target — Where It Can Be Used
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) restricts an annotation to only compile onto the listed kinds of declaration, catching misuse (like annotating a variable that was meant for classes only) at compile time.
🎯 Use-Site Targets — Why Kotlin Needs Them
A single Kotlin val/var property can compile down into several distinct underlying JVM elements at once — a backing field, a getter method, a setter method (for var), and sometimes a constructor parameter. An annotation applied to the property alone is ambiguous about which of those it should attach to:
Use-Site Target Prefixes
| Prefix | Targets |
|---|---|
| @property: | The Kotlin property itself (the default when none is specified, where unambiguous) |
| @field: | The underlying backing field |
| @get: | The generated getter function |
| @set: | The generated setter function (var only) |
| @param: | The constructor parameter |
| @setparam: | The setter's parameter specifically |
This is purely a Kotlin-specific concern — it exists because Kotlin properties are a higher-level abstraction over what Java expresses as separate field + getter + setter methods. Annotations that are Java-interop-focused (like @JvmField or Jackson/Gson's JSON annotations) very often need a specific use-site target to land on the correct underlying element.
If an annotation could reasonably apply to more than one target and none is specified, Kotlin raises a compile error asking for a use-site target rather than silently guessing. This is a safety feature, not friction — a wrongly-placed annotation (e.g. a JSON library annotation that ends up on the field instead of the getter) can fail silently at runtime instead of at compile time if the compiler didn't insist on clarity here.
💻 Coding Challenges
Challenge 1: Inspecting a Class with KClass
Using a data class Product(val name: String, val price: Double, val inStock: Boolean), write a function fun describe(obj: Any) that uses reflection (obj::class.memberProperties) to print every property name and value on the given object, one per line, in the format name: value.
Goal: Practice reading an object's properties generically via reflection, without hardcoding property names.
Challenge 2: A Custom Annotation with a Reader
Declare an annotation @Target(AnnotationTarget.CLASS) annotation class Author(val name: String) with RUNTIME retention. Apply it to a class with a made-up author name, then write a function fun printAuthor(kClass: KClass<*>) that finds the Author annotation on the given class (if present) and prints its name, or "No author" if absent.
Goal: Practice declaring, applying, and reading back a custom annotation via reflection.
Challenge 3: Use-Site Targets
Given the annotations @Target(AnnotationTarget.FIELD) annotation class Sensitive and @Target(AnnotationTarget.PROPERTY_GETTER) annotation class Logged, write a class with a property that applies @Sensitive to its backing field and @Logged to its getter, using the correct use-site target prefix for each. Add a comment explaining why a use-site target is required for each one.
Goal: Practice choosing the correct use-site target prefix based on an annotation's declared @Target.
Day-to-day application code rarely reaches for reflection directly — it's the mechanism underneath tools that feel automatic: JSON libraries mapping objects to fields, dependency injection frameworks wiring up constructors, testing frameworks discovering @Test-annotated functions. Understanding it here mostly pays off by demystifying how those tools work, not as something to reach for constantly in application code.
🎯 What's Next
Next chapter: Advanced Patterns — value classes, operator overloading, contracts, and type aliases.