5 Minutes
Kotlin Annotations Explained: Guide for Android Developers
Fix Bugs Faster! Log Collection Made Easy
How Kotlin Annotations work
Kotlin annotations allow compilers or libraries to understand our code.
These metadata tags don’t directly change code logic, but they help modify how it is interpreted, optimized, or validated.
This simplifies Android development by automating repetitive tasks and ensuring consistent code behavior. It also improves code readability, reduces boilerplate code, and introduces automated checks and generation.
Kotlinn annotations can be applied to classes, functions, properties, parameters, and even entire files. For example, in Android, annotations like @NonNull or @SerializedName help ensure type safety or configure serialization behavior.
Why We Use Kotlin annotations
Kotlin annotations have all kinds of use cases in day-to-day coding. Here are some of the most common:
- Code validation: Enforcing compile-time rules (e.g., 
@NonNull). - Dependency injection: Used in frameworks like Dagger or Hilt.
 - Serialization: Mapping data between JSON and Kotlin objects.
 - Testing: Simplifying test configuration with 
@Test. - Documentation: Making APIs more descriptive with annotations like 
@Deprecated. 
Really, they’ll improve all aspects of your coding game. Ready to start using them? Let’s dive in.
Built-in Kotlin Annotations
Kotlin provides several built-in annotations that you’ll frequently encounter while developing Android apps.
@Deprecated
This marks a function, class, or property as outdated.
@Deprecated("Use newMethod() instead")
fun oldMethod() {}
The compiler warns you when the deprecated element has been used.
@JvmStatic
This is used in companion objects to make methods callable from Java without needing the instance.
companion object {
    @JvmStatic fun show() = println("Static call from Java")
}
@JvmOverloads
Generates overloaded methods for Java interoperability when using default parameters.
fun greet(name: String = "User") = println("Hello, $name")
@JvmField
Exposes a Kotlin property as a public Java field.
class Config {
    @JvmField val version = "1.0"
}
@Synchronized
Ensures that a function is thread-safe.
@Synchronized fun safeIncrement() { /* thread-safe code */ }
How to Apply Kotlin Annotations
Now we’ve looked at the most common out-of-the-box examples of Kotlin annotations, let’s get to work applying them.
Actually, the application process is very simple—you just place the annotation before the element you want to modify. Like this.
class User(
    @SerializedName("user_name") val name: String,
    @NonNull val email: String
)
You can also use multiple annotations:
@Synchronized
@Test
fun loadUserData() { /* ... */ }
Remember: annotations can apply to classes, functions, properties, parameters, or constructors, depending on the target.
How to declare Kotlin Annotations
Kotlin annotations are generally easier to declare than regular classes, because they’re so focused and limited in scope.
You can create your own annotations using the annotation class keyword.
annotation class LogExecution(val level: String = "INFO")
To annotate a constructor
annotation class Inject
class Service @Inject constructor(private val repo: Repo)
To annotate a property
annotation class FieldName(val name: String)
class User(@FieldName("user_id") val id: Int)
Specific types of annotations
These will likely come up a lot during your day-to-day work as a dev.
@Target
Specifies where the annotation can be used.
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
annotation class MyAnnotation
@Retention
Defines how long the annotation is retained.
@Retention(AnnotationRetention.RUNTIME)
annotation class RuntimeAnnotation
Use cases include:
| Retention Type | Availability | Use Case Example | 
|---|---|---|
| SOURCE | Discarded at compile-time | Code linting | 
| BINARY | Stored in .class but not runtime | JVM processing | 
| RUNTIME | Available during runtime | Reflection-based logic | 
@Repeatable
Allows multiple instances of the same annotation on one element.
@Repeatable
annotation class Permission(val value: String)
@MustBeDocumented
Ensures that an annotation appears in generated API docs.
@MustBeDocumented
annotation class PublicAPI
Nested Declarations in Annotations
Means annotations can contain other annotations or enums.
annotation class Log(val level: Level) {
    enum class Level { INFO, WARN, ERROR }
}
How to process Kotlin Annotations
If you’ve been working with Kotlin for a while, you’ll know that it compiles code differently to Java, and this can create challenges when processing.
Thankfully, annotation processing is pretty flexible, and we can call on the Kotlin Annotation Processing Tool, or KAPT, and its successors (as we’ll show below).
Kotlin annotations can be processed at compile-time or runtime, depending on retention policy.
Compile-time processing
Compile-time processors like KAPT generate code before compilation.
Example:
@AutoGenerated class Mapper { /* generated by KAPT */ }
This is used in libraries like Dagger or Room.
Runtime Processing
At runtime, annotations are accessed through reflection:
val annotation = MyClass::class.annotations.find { it is LogExecution }
This is useful when behavior depends on runtime data, like dynamic logging or validation.
| Processing Type | When It Happens | Typical Tool | Use Case | 
|---|---|---|---|
| Compile-Time | During build | KAPT | Dependency injection | 
| Runtime | During execution | Reflection | Logging, validation | 
Kotlin processor tools
As well as KAPT, Kotlin’s standard compile-time processor for generating source code, we can draw on a whole bunch of tools including:
- KSP (Kotlin Symbol Processing): An alternative to KAPT.
 - Annotation Processing APIs: Used for custom build logic and code generation.
 
Each tool allows Android developers to extend Kotlin’s functionality and integrate seamlessly with frameworks like Room, Glide, or Hilt.
How to create custom Kotlin annotations
Creating custom annotations lets you automate logic and reduce boilerplate. You can also:
- Express domain intent.
 - Add safety to your code configuration.
 - Promote consistent coding practices across teams.
 - Create cleaner, more readable APIs.
 
Here’s an example:
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class LogExecution(val message: String)
You can then process this annotation with reflection or a processor to log method calls automatically.
How to ensure Java-interoperability with annotations
Kotlin is fully interoperable with Java, but annotations require special handling to ensure compatibility.
Using the ‘JVM’ (Java Virtual Machine) prefix, we can make Kotlin behave more like Java at the bytecode level and ensure that Kotlin classes behave predictably when called from Java codebases.
Here are some common ones:
| Kotlin Annotation | Java Equivalent | Purpose | 
|---|---|---|
@JvmStatic | static method | Allows static access | 
@JvmOverloads | Overloaded methods | Enables default arguments in Java | 
@JvmField | Public field | Avoids getters/setters | 
@Throws | throws declaration | Propagates exceptions to Java | 
Using these annotations ensures that Kotlin classes behave predictably when called from Java codebases.
Exception handling
Like any aspect of programming, Kotlin annotations can throw up unusual situations and edge cases. When working with annotations that may influence runtime behavior, it’s important to handle potential exceptions gracefully.
Common exceptions include:
ClassNotFoundException— when referencing missing annotations.IllegalAccessException— when reflection accesses restricted members.NullPointerException— when annotations assume non-null targets.
Be sure to always wrap reflection logic in try-catch blocks and validate annotation presence before use.
Example:
try {
    val log = method.getAnnotation(LogExecution::class.java)
} catch (e: Exception) {
    println("Annotation error: ${e.message}")
}
Summary
Kotlin annotations are powerful tools for adding metadata, enforcing constraints, and automating behavior in Android apps.
- They simplify dependency injection, serialization, and validation.
 - You can create custom annotations using 
annotation class. - Processing occurs at compile-time (KAPT/KSP) or runtime (reflection).
 - Java interoperability is maintained via 
@Jvm*annotations. 
Hopefully you’ve now got a good grounding in this aspect of programming, so you can go forward and incorporate into your developer’s toolkit. But if you still have questions, then let’s chat: email our support team and we’ll help you work through your question.
Expect The Unexpected!
Debug Faster With Bugfender