Skip to Content
Course content

54: Annotations in Kotlin

Click on the "Edit" button in the top corner of the screen to edit your slide content.

At some point in your career, you're going to run into a problem where you need to attach extra information to a piece of code—not information that changes how the logic executes in the moment, but information that something else (like a framework or a validator) can use to decide how to treat that code. This is exactly what annotations are for.

The manual checklist headache

Imagine we're building a system to validate user profiles. We have a User data class, and we need to ensure that the username isn't empty and the email looks like an actual email. The naive way to handle this is to write a dedicated validation function that manually checks every field.

data class User(val username: String, val email: String, val bio: String)

fun validateUser(user: User) {
    if (user.username.isEmpty()) throw Exception("Username cannot be empty")
    if (!user.email.contains("@")) throw Exception("Invalid email")
    // bio can be empty, so we leave it alone
}

This works fine when you have three fields. But as the project grows, you'll end up with fifty fields across twenty different data classes. You'll find yourself writing the same if (field.isEmpty()) logic over and over again. It's repetitive, and it's incredibly easy to add a new field to a class and simply forget to update the validation function. I've seen entire production outages caused by someone adding a "Required" field to a database model but forgetting to add the corresponding if statement in the validation layer.

Tagging your data for later

The better way is to stop treating validation as a sequence of hardcoded checks and start treating it as a set of rules attached to the data itself. We can do this by defining our own annotations. Instead of a manual checklist, we "tag" the properties we care about.

First, we need to tell Kotlin where these annotations can actually be used and how long they should stick around. If you don't specify the @Retention, the annotation is discarded after compilation, which means you can't see it at runtime via reflection. For our validator, we need RUNTIME retention.

@Target(AnnotationTarget.FIELD)
@Retention(AnnotationRetention.RUNTIME)
annotation class NotEmpty

@Target(AnnotationTarget.FIELD)
@Retention(AnnotationRetention.RUNTIME)
annotation class EmailFormat

Now, our User class becomes declarative. We aren't writing logic; we're describing the state we expect:

data class User(
    @NotEmpty val username: String,
    @EmailFormat val email: String,
    val bio: String
)

The trade-off: Reflection vs. Boilerplate

Now you might be wondering: "Wait, I still have to write the code that actually checks the annotations, right?" Yes, you do. But here is the win: you only write that logic once for the entire application.

By using reflection, we can write a single validator that looks at any object, finds the fields marked with @NotEmpty, and checks if they are blank. I'll be honest with you—reflection is slower than a direct if statement. In a tight loop running millions of times a second, this would be a mistake. But for a user registration form? The performance hit is nanoseconds, while the developer productivity gain is massive. You've traded a tiny bit of CPU time for a system where adding a new validation rule is as simple as adding one word above a property.

The real magic happens when you realize this is how almost every modern JVM framework operates. Spring uses @Autowired, JUnit uses @Test, and Hibernate uses @Entity. They aren't magic; they're just scanning your code for these tags and executing a predefined behavior based on what they find.




📋 Practical Task

Build a Permission-Based Method Guard

You are building a security module where certain functions should only be executed if the user has a specific permission level. Instead of wrapping every function body in an if (user.hasPermission(...)) block, you will implement an annotation-based guard.

Your Task:

  • Create an annotation called @RequiresPermission that accepts a String parameter (the name of the required permission).
  • Ensure the annotation is targeted at AnnotationTarget.FUNCTION and has AnnotationRetention.RUNTIME.
  • Create a SecurityManager class with a function called executeSecurely(func: KFunction<*>, user: User).
  • Inside executeSecurely, use reflection to check if the function is annotated with @RequiresPermission.
  • If the annotation exists, check if the user object possesses the required permission string. If they don't, throw a SecurityException. If they do (or if there is no annotation), call the function.

Hint: You will need to import kotlin.reflect.full.* to access the findAnnotation extension function.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.