Kotlin - Annotations

Try it: Use annotations to add metadata to your code and enable powerful framework integrations.

What are Annotations?

Annotations are metadata that provide information about code elements. They don't affect program execution directly but can be processed by tools, frameworks, and the compiler.

Basic Annotation Declaration

// Simple annotation
annotation class Author(val name: String)

// Multiple parameters
annotation class Version(
    val major: Int,
    val minor: Int,
    val patch: Int = 0
)

// Usage
@Author("John Doe")
@Version(major = 1, minor = 2)
class MyLibrary {
    fun calculate(): Int = 42
}

Annotation Targets

@Target(
    AnnotationTarget.CLASS,
    AnnotationTarget.FUNCTION,
    AnnotationTarget.PROPERTY,
    AnnotationTarget.CONSTRUCTOR,
    AnnotationTarget.FIELD,
    AnnotationTarget.VALUE_PARAMETER
)
annotation class Documented(val description: String)

@Target(AnnotationTarget.PROPERTY)
annotation class JsonField(val name: String)

@Target(AnnotationTarget.FUNCTION)
annotation class Benchmark

// Usage examples
@Documented("Main application class")
class App {
    
    @JsonField("user_name")
    val username: String = "admin"
    
    @Benchmark
    @Documented("Performance critical method")
    fun processData(): String {
        return "processed"
    }
}

Annotation Retention

// Only available at compile time
@Retention(AnnotationRetention.SOURCE)
annotation class CompileTimeOnly

// Available in bytecode but not at runtime
@Retention(AnnotationRetention.BINARY)
annotation class BytecodeOnly

// Available at runtime via reflection (default)
@Retention(AnnotationRetention.RUNTIME)
annotation class RuntimeAvailable(val value: String)

// Example: Deprecation with source retention
@Retention(AnnotationRetention.SOURCE)
@Target(AnnotationTarget.FUNCTION)
annotation class Deprecated(val message: String)

@Deprecated("Use newMethod() instead")
fun oldMethod() {
    // Compiler warning but no runtime overhead
}

Meta-annotations

@Target(AnnotationTarget.ANNOTATION_CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class ApiAnnotation

@ApiAnnotation
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class GET(val path: String)

@ApiAnnotation
@Target(AnnotationTarget.FUNCTION)  
@Retention(AnnotationRetention.RUNTIME)
annotation class POST(val path: String)

// Usage in REST API
class UserController {
    @GET("/users/{id}")
    fun getUser(id: String): User = User(id, "John")
    
    @POST("/users")
    fun createUser(user: User): User = user
}

Built-in Annotations

JVM Interoperability

class JavaInterop {
    
    // Generate static methods for Java
    companion object {
        @JvmStatic
        fun staticMethod() = "accessible from Java"
        
        @JvmField
        val staticField = "public static final field"
    }
    
    // Control property naming in Java
    @get:JvmName("getSpecialName")
    @set:JvmName("setSpecialName")
    var customProperty: String = ""
    
    // Handle overloads for Java
    @JvmOverloads
    fun greet(name: String = "World", enthusiastic: Boolean = false): String {
        return if (enthusiastic) "Hello, $name!!!" else "Hello, $name"
    }
    
    // Throw checked exceptions for Java
    @Throws(IOException::class)
    fun riskyOperation() {
        // might throw IOException
    }
}

Serialization Annotations

import kotlinx.serialization.*
import kotlinx.serialization.json.*

@Serializable
data class User(
    @SerialName("user_id")
    val id: String,
    
    @SerialName("full_name")
    val name: String,
    
    @SerialName("email_address")
    val email: String,
    
    // Optional field with default
    val isActive: Boolean = true,
    
    // Transient field - not serialized
    @Transient
    val temporaryData: String = ""
)

@Serializable
data class ApiResponse<T>(
    val success: Boolean,
    val data: T? = null,
    val error: String? = null,
    
    @SerialName("timestamp")
    val createdAt: Long = System.currentTimeMillis()
)

fun main() {
    val user = User("123", "John Doe", "[email protected]")
    val response = ApiResponse(success = true, data = user)
    
    val json = Json.encodeToString(response)
    println(json)
    
    // Deserialize back
    val parsed = Json.decodeFromString<ApiResponse<User>>(json)
    println(parsed.data?.name)
}

Custom Annotations

Validation Framework

@Target(AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.RUNTIME)
annotation class NotEmpty(val message: String = "Field cannot be empty")

@Target(AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.RUNTIME)
annotation class Email(val message: String = "Invalid email format")

@Target(AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.RUNTIME)
annotation class Range(
    val min: Int = Int.MIN_VALUE,
    val max: Int = Int.MAX_VALUE,
    val message: String = "Value out of range"
)

@Target(AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.RUNTIME)
annotation class Pattern(
    val regex: String,
    val message: String = "Value doesn't match pattern"
)

data class UserRegistration(
    @NotEmpty("Username is required")
    val username: String,
    
    @Email("Please provide a valid email")
    @NotEmpty("Email is required")
    val email: String,
    
    @Range(min = 18, max = 120, message = "Age must be between 18 and 120")
    val age: Int,
    
    @Pattern(
        regex = "^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d]{8,}$",
        message = "Password must be at least 8 characters with letters and numbers"
    )
    val password: String,
    
    @Pattern(regex = "^\\+?[1-9]\\d{1,14}$", message = "Invalid phone number")
    val phone: String
)

object Validator {
    fun validate(obj: Any): List<ValidationError> {
        val errors = mutableListOf<ValidationError>()
        val kClass = obj::class
        
        kClass.memberProperties.forEach { property ->
            val value = property.get(obj)
            
            // Check @NotEmpty
            property.findAnnotation<NotEmpty>()?.let { annotation ->
                if (value is String && value.isEmpty()) {
                    errors.add(ValidationError(property.name, annotation.message))
                }
            }
            
            // Check @Email
            property.findAnnotation<Email>()?.let { annotation ->
                if (value is String && !value.matches(Regex("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$"))) {
                    errors.add(ValidationError(property.name, annotation.message))
                }
            }
            
            // Check @Range
            property.findAnnotation<Range>()?.let { annotation ->
                if (value is Int && (value < annotation.min || value > annotation.max)) {
                    errors.add(ValidationError(property.name, annotation.message))
                }
            }
            
            // Check @Pattern
            property.findAnnotation<Pattern>()?.let { annotation ->
                if (value is String && !value.matches(Regex(annotation.regex))) {
                    errors.add(ValidationError(property.name, annotation.message))
                }
            }
        }
        
        return errors
    }
}

data class ValidationError(val field: String, val message: String)

fun main() {
    val registration = UserRegistration(
        username = "",
        email = "invalid-email",
        age = 15,
        password = "weak",
        phone = "invalid"
    )
    
    val errors = Validator.validate(registration)
    errors.forEach { error ->
        println("${error.field}: ${error.message}")
    }
}

Database Mapping Annotations

@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class Table(val name: String)

@Target(AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.RUNTIME)
annotation class Column(val name: String = "", val nullable: Boolean = true)

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

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

@Table("users")
data class User(
    @Id
    @GeneratedValue
    @Column("user_id")
    val id: Long = 0,
    
    @Column("username", nullable = false)
    val username: String,
    
    @Column("email_address", nullable = false)
    val email: String,
    
    @Column("created_at")
    val createdAt: Long = System.currentTimeMillis(),
    
    @Column("is_active")
    val isActive: Boolean = true
)

object SimpleORM {
    fun generateCreateTable(entityClass: KClass<*>): String {
        val tableAnnotation = entityClass.findAnnotation<Table>()
            ?: throw IllegalArgumentException("Class must be annotated with @Table")
        
        val tableName = tableAnnotation.name
        val columns = mutableListOf<String>()
        
        entityClass.memberProperties.forEach { property ->
            val columnAnnotation = property.findAnnotation<Column>()
            if (columnAnnotation != null) {
                val columnName = columnAnnotation.name.ifEmpty { property.name }
                val nullable = if (columnAnnotation.nullable) "" else " NOT NULL"
                val type = when (property.returnType.classifier) {
                    String::class -> "VARCHAR(255)"
                    Int::class, Long::class -> "BIGINT"
                    Boolean::class -> "BOOLEAN"
                    else -> "TEXT"
                }
                
                var columnDef = "$columnName $type$nullable"
                
                if (property.hasAnnotation<Id>()) {
                    columnDef += " PRIMARY KEY"
                }
                
                if (property.hasAnnotation<GeneratedValue>()) {
                    columnDef += " AUTO_INCREMENT"
                }
                
                columns.add(columnDef)
            }
        }
        
        return "CREATE TABLE $tableName (\n  ${columns.joinToString(",\n  ")}\n);"
    }
    
    fun generateInsert(entity: Any): String {
        val entityClass = entity::class
        val tableAnnotation = entityClass.findAnnotation<Table>()
            ?: throw IllegalArgumentException("Class must be annotated with @Table")
        
        val tableName = tableAnnotation.name
        val columns = mutableListOf<String>()
        val values = mutableListOf<String>()
        
        entityClass.memberProperties.forEach { property ->
            val columnAnnotation = property.findAnnotation<Column>()
            if (columnAnnotation != null && !property.hasAnnotation<GeneratedValue>()) {
                val columnName = columnAnnotation.name.ifEmpty { property.name }
                columns.add(columnName)
                
                val value = property.get(entity)
                values.add(when (value) {
                    is String -> "'$value'"
                    is Number -> value.toString()
                    is Boolean -> value.toString()
                    else -> "'$value'"
                })
            }
        }
        
        return "INSERT INTO $tableName (${columns.joinToString(", ")}) VALUES (${values.joinToString(", ")});"
    }
}

fun main() {
    // Generate DDL
    val createTableSQL = SimpleORM.generateCreateTable(User::class)
    println("DDL:")
    println(createTableSQL)
    
    // Generate INSERT
    val user = User(username = "johndoe", email = "[email protected]")
    val insertSQL = SimpleORM.generateInsert(user)
    println("\nDML:")
    println(insertSQL)
}

Testing Annotations

@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class Test

@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class BeforeEach

@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class AfterEach

@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class Timeout(val milliseconds: Long)

@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class ExpectedException(val exception: KClass<out Throwable>)

class CalculatorTest {
    private lateinit var calculator: Calculator
    
    @BeforeEach
    fun setup() {
        calculator = Calculator()
        println("Setup completed")
    }
    
    @AfterEach
    fun cleanup() {
        println("Cleanup completed")
    }
    
    @Test
    fun testAddition() {
        val result = calculator.add(2, 3)
        assert(result == 5) { "Expected 5, got $result" }
    }
    
    @Test
    @Timeout(1000)
    fun testPerformance() {
        // Long running test
        repeat(1000000) {
            calculator.add(1, 1)
        }
    }
    
    @Test
    @ExpectedException(ArithmeticException::class)
    fun testDivisionByZero() {
        calculator.divide(10, 0)
    }
}

class Calculator {
    fun add(a: Int, b: Int): Int = a + b
    fun divide(a: Int, b: Int): Int = a / b
}

object SimpleTestRunner {
    fun runTests(testClass: KClass<*>) {
        val instance = testClass.java.getDeclaredConstructor().newInstance()
        val methods = testClass.memberFunctions
        
        val beforeEachMethods = methods.filter { it.hasAnnotation<BeforeEach>() }
        val afterEachMethods = methods.filter { it.hasAnnotation<AfterEach>() }
        val testMethods = methods.filter { it.hasAnnotation<Test>() }
        
        var passed = 0
        var failed = 0
        
        testMethods.forEach { testMethod ->
            try {
                // Run @BeforeEach methods
                beforeEachMethods.forEach { it.call(instance) }
                
                // Check timeout
                val timeout = testMethod.findAnnotation<Timeout>()?.milliseconds ?: Long.MAX_VALUE
                val startTime = System.currentTimeMillis()
                
                // Run test - might throw expected exception
                val expectedException = testMethod.findAnnotation<ExpectedException>()
                try {
                    testMethod.call(instance)
                    
                    if (expectedException != null) {
                        println("FAIL: ${testMethod.name} - Expected ${expectedException.exception.simpleName}")
                        failed++
                    } else {
                        val duration = System.currentTimeMillis() - startTime
                        if (duration > timeout) {
                            println("FAIL: ${testMethod.name} - Timeout (${duration}ms > ${timeout}ms)")
                            failed++
                        } else {
                            println("PASS: ${testMethod.name}")
                            passed++
                        }
                    }
                } catch (e: Exception) {
                    if (expectedException != null && e::class == expectedException.exception) {
                        println("PASS: ${testMethod.name} - Expected exception caught")
                        passed++
                    } else {
                        println("FAIL: ${testMethod.name} - ${e.message}")
                        failed++
                    }
                }
                
                // Run @AfterEach methods
                afterEachMethods.forEach { it.call(instance) }
                
            } catch (e: Exception) {
                println("ERROR: ${testMethod.name} - ${e.message}")
                failed++
            }
        }
        
        println("\nTest Results: $passed passed, $failed failed")
    }
}

fun main() {
    SimpleTestRunner.runTests(CalculatorTest::class)
}

Common Pitfalls

  • Retention: Remember to set RUNTIME retention for reflection-based processing
  • Target: Specify appropriate targets to prevent misuse of annotations
  • Performance: Runtime annotation processing can be expensive; consider compile-time alternatives
  • Proguard: Annotation-based code may be removed by code obfuscation tools

Best Practices

  • Use meaningful names and provide good documentation for custom annotations
  • Prefer compile-time processing (KSP) over runtime reflection when possible
  • Keep annotation parameters simple and provide sensible defaults
  • Use meta-annotations to group related annotations
  • Consider creating annotation processors for better performance and type safety

Practice Exercises

  1. Create a logging framework that uses annotations to control log levels
  2. Build a caching system that caches method results based on annotations
  3. Implement a permissions system using annotations on methods and classes

Architecture Notes

  • Framework Integration: Annotations are essential for modern frameworks (Spring, Retrofit, Room)
  • Code Generation: Enable powerful compile-time code generation tools
  • Documentation: Can serve as executable documentation that stays in sync with code
  • Aspect-Oriented Programming: Enable cross-cutting concerns like logging, security, transactions