Annotations and Reflection
ver. 1.0.0
Use Kotlin annotations and reflection to inspect, customize, serialize, and construct types you don’t know at compile time.
This unit teaches how to attach and declare annotations, control their targets and retention, and use Kotlin’s reflection API (KClass, KCallable, KFunction, KProperty) to inspect and manipulate classes at runtime. You will learn to read use-site targets, pass compile-time constant arguments (const val and KClass references), build a runtime JSON serializer that respects annotations, and construct objects from parsed data using
callBy. The unit closes by explaining reflection’s runtime cost and when to prefer it.
The unit explains how annotations and reflection work together to let you write code that operates on classes you do not know at compile time. It covers annotation syntax and constraints (annotation arguments must be compile-time constants — use const val and KClass references for class parameters), and the ambiguity around annotated Kotlin properties (a property can map to a field, getter, setter, or constructor parameter), which is resolved with use-site targets like @get: or @field:.
You learn to declare annotation classes with parameters (for example annotation class JsonName(val name: String)) and control where and how long annotations apply using meta-annotations such as @Target and @Retention. The unit highlights differences in default retention and how to ensure annotations are available at runtime.
The core reflection API is introduced through four interfaces: KClass (type metadata), KCallable (callable member), KFunction (functions and constructors), and KProperty (properties). Practical idioms are shown: obtaining a KClass via obj::class, iterating members with .memberProperties, and invoking callables without static type knowledge.
These pieces are combined into a serializer that walks an object’s properties with memberProperties, reads values, and emits JSON; annotations on properties let the class customize serialization (e.g., rename fields). Deserialization is addressed by constructing objects from name/value maps: call requires every parameter in order, while callBy accepts a map and allows defaults to fill missing parameters — making it suitable for parsing optional JSON fields.
Finally, the unit discusses the tradeoffs and costs of reflection: it shifts checks from compile time to runtime, has performance and complexity implications, and is best used when types cannot be known ahead of time (libraries, frameworks, generic serializers) rather than as a default approach. The next step contrasts this runtime flexibility with compile-time techniques such as building DSLs to achieve similar goals without reflection.
Materials
Source document
- Kotlin in Action, Second Edition, Sebastian Aigner, Roman Elizarov, Svetlana Isakova, and Dmitry Jemerov, Manning, April 2024 — Link — Page 289-319