Skip to Content
Course content

216: Retention Policies for Custom Annotations

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

Up until now, you've probably been creating annotations and just assuming they'll be there when you need them. But if you've ever tried to check for an annotation using reflection and found that it simply "disappeared," you've run into the retention policy problem. Essentially, you have to tell the Kotlin compiler how long that metadata should survive.

Wait, why do I even need to specify a retention policy?

Think of annotations as sticky notes. Some sticky notes are just for the person writing the code (the developer), some are for the person assembling the parts (the compiler), and some need to stay attached to the product even after it's shipped and running in a customer's environment (the JVM). If you don't specify a policy, Kotlin defaults to AnnotationRetention.RUNTIME, which is helpful, but relying on defaults in a professional codebase is a habit I'd suggest breaking.

By explicitly setting the retention, you're optimizing your compiled bytecode. Why keep metadata in the final binary if you only needed it to trigger a warning in the IDE? It's a small win, but it's the difference between a "it works" project and a production-ready one.

What is the actual difference between SOURCE, BINARY, and RUNTIME?

There are three levels, and the distinction is all about where the annotation "lives" during the lifecycle of your code.

  • SOURCE: These are stripped out during compilation. They never even make it into the .class files. I use these mostly for things like @Suppress or custom linting rules where the compiler just needs a hint.
  • BINARY: These are stored in the .class files, but the JVM ignores them when the class is loaded into memory. You can't find these using reflection, but a bytecode analysis tool (like ProGuard or a custom compiler plugin) can still see them.
  • RUNTIME: These are the heavy lifters. They stay in the binary and are loaded into the JVM. This is the only way you can use reflection to ask a class, "Hey, do you have this annotation?"
@Retention(AnnotationRetention.SOURCE)
annotation class InternalDraft // Only visible in the editor

@Retention(AnnotationRetention.BINARY)
annotation class OptimizedByCompiler // Visible in bytecode, not at runtime

@Retention(AnnotationRetention.RUNTIME)
annotation class SensitiveData // Visible everywhere, including reflection

How do I actually use a RUNTIME annotation in a real feature?

Let's say you're building a logging system and you want to make sure you don't accidentally leak PII (Personally Identifiable Information) into your logs. You can create a @SensitiveData annotation and use reflection to mask those fields.

If you set this to BINARY or SOURCE, the following logic would simply fail to find the annotation and would print the raw password. You must use RUNTIME here.

@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.PROPERTY)
annotation class SensitiveData

data class UserProfile(
    val username: String,
    @SensitiveData val passwordHash: String,
    @SensitiveData val socialSecurityNumber: String
)

fun maskSensitiveInfo(obj: Any): String {
    val clazz = obj::class
    val fields = clazz.members.filterIsInstance<kotlin.reflect.KProperty1<Any, *>>
    
    return fields.joinToString(", ") { prop ->
        val value = prop.get(obj)
        val isSensitive = prop.annotations.any { it is SensitiveData }
        val displayValue = if (isSensitive) "****" else value
        "${prop.name}=$displayValue"
    }
}

// Usage
val user = UserProfile("kotlin_dev", "secret_hash_123", "999-00-1111")
println(maskSensitiveInfo(user)) 
// Output: username=kotlin_dev, passwordHash=****, socialSecurityNumber=****

I've seen developers spend hours debugging why prop.annotations was returning an empty list, only to realize they forgot the @Retention tag or accidentally set it to BINARY. Always double-check that if you're using KClass or javaClass to find an annotation, RUNTIME is the only way to go.




📋 Practical Task

Implementing a Runtime-Based Validation Engine

You are tasked with building a simple validation engine that checks if specific fields in a data class are "required" before a record is saved to a database.

Requirements:

  • Create a custom annotation called @RequiredField.
  • Set the RetentionPolicy so that the annotation can be detected at runtime via reflection.
  • Create a data class Product with at least three properties (e.g., name, sku, description). Mark only some of them as @RequiredField.
  • Write a function validateProduct(product: Product): List<String> that uses reflection to inspect the properties of the product. If a property is marked as @RequiredField but its value is null or an empty string, add the name of that property to a list of validation errors.
  • Test your function with a Product instance that has missing required fields and print the resulting list of errors.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.