Kotlin - Reflection

Try it: Use Kotlin's reflection API to analyze classes at runtime and build flexible applications.

What is Reflection?

Reflection allows you to examine and manipulate program structure at runtime. Kotlin provides interoperability with Java reflection plus its own reflection API.

Basic Class Reflection

import kotlin.reflect.KClass
import kotlin.reflect.full.*

class User(val name: String, val age: Int) {
    fun greet() = "Hello, I'm $name"
}

fun main() {
    // Get class reference
    val userClass: KClass<User> = User::class
    
    // Class information
    println("Class name: ${userClass.simpleName}")
    println("Qualified name: ${userClass.qualifiedName}")
    
    // Check if class is data class
    println("Is data class: ${userClass.isData}")
    
    // Get constructors
    userClass.constructors.forEach { constructor ->
        println("Constructor parameters: ${constructor.parameters.map { it.name }}")
    }
}

// Output:
// Class name: User
// Qualified name: User
// Is data class: false
// Constructor parameters: [name, age]

Property Reflection

import kotlin.reflect.full.*

data class Person(var firstName: String, var lastName: String) {
    val fullName: String get() = "$firstName $lastName"
}

fun main() {
    val person = Person("John", "Doe")
    val personClass = Person::class
    
    // Get all properties
    personClass.memberProperties.forEach { property ->
        println("Property: ${property.name}, Type: ${property.returnType}")
        
        // Get property value
        val value = property.get(person)
        println("Value: $value")
        
        // Check if mutable
        if (property is kotlin.reflect.KMutableProperty1) {
            println("Property is mutable")
        }
    }
    
    // Access specific property
    val firstNameProperty = personClass.memberProperties
        .first { it.name == "firstName" }
    
    // Modify mutable property
    if (firstNameProperty is kotlin.reflect.KMutableProperty1) {
        firstNameProperty.set(person, "Jane")
        println("Updated: ${person.firstName}")
    }
}

Function Reflection

import kotlin.reflect.full.*

class Calculator {
    fun add(a: Int, b: Int): Int = a + b
    fun multiply(x: Double, y: Double): Double = x * y
    private fun secret() = "Hidden"
}

fun main() {
    val calc = Calculator()
    val calcClass = Calculator::class
    
    // Get all functions
    calcClass.memberFunctions.forEach { function ->
        println("Function: ${function.name}")
        println("Parameters: ${function.parameters.drop(1).map { "${it.name}: ${it.type}" }}")
        println("Return type: ${function.returnType}")
        println("Visibility: ${function.visibility}")
        println()
    }
    
    // Call function by name
    val addFunction = calcClass.memberFunctions
        .first { it.name == "add" }
    
    val result = addFunction.call(calc, 5, 3)
    println("Dynamic call result: $result")
}

Annotation Reflection

@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY, AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class ApiEndpoint(val path: String, val method: String = "GET")

@Target(AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.RUNTIME)
annotation class JsonProperty(val name: String)

@ApiEndpoint("/users", "POST")
data class UserRequest(
    @JsonProperty("user_name") val username: String,
    @JsonProperty("user_email") val email: String
) {
    @ApiEndpoint("/validate", "POST")
    fun validate(): Boolean = username.isNotEmpty() && email.contains("@")
}

fun main() {
    val userClass = UserRequest::class
    
    // Class annotations
    userClass.annotations.forEach { annotation ->
        when (annotation) {
            is ApiEndpoint -> {
                println("API: ${annotation.method} ${annotation.path}")
            }
        }
    }
    
    // Property annotations
    userClass.memberProperties.forEach { property ->
        property.annotations.forEach { annotation ->
            when (annotation) {
                is JsonProperty -> {
                    println("Property ${property.name} maps to JSON field '${annotation.name}'")
                }
            }
        }
    }
    
    // Function annotations
    userClass.memberFunctions.forEach { function ->
        function.annotations.forEach { annotation ->
            when (annotation) {
                is ApiEndpoint -> {
                    println("Function ${function.name}: ${annotation.method} ${annotation.path}")
                }
            }
        }
    }
}

Generic Type Reflection

import kotlin.reflect.full.*
import kotlin.reflect.typeOf

class Container<T>(val value: T) {
    fun getValue(): T = value
}

inline fun <reified T> analyzeType() {
    val type = typeOf<T>()
    println("Type: $type")
    println("Classifier: ${type.classifier}")
    println("Arguments: ${type.arguments}")
}

fun main() {
    // Analyze different generic types
    analyzeType<List<String>>()
    analyzeType<Map<String, Int>>()
    analyzeType<Container<Person>>()
    
    // Runtime type checking
    val stringList: List<String> = listOf("a", "b", "c")
    val listType = typeOf<List<String>>()
    
    println("Type matches: ${stringList::class.createType() == listType}")
}

Practical Applications

JSON Serialization Framework

import kotlin.reflect.full.*

@Target(AnnotationTarget.PROPERTY)
annotation class JsonIgnore

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

object SimpleJsonSerializer {
    fun serialize(obj: Any): String {
        val kClass = obj::class
        val properties = kClass.memberProperties
        
        val jsonFields = properties
            .filter { !it.hasAnnotation<JsonIgnore>() }
            .map { property ->
                val jsonName = property.findAnnotation<JsonName>()?.name ?: property.name
                val value = property.get(obj)
                val jsonValue = when (value) {
                    is String -> "\"$value\""
                    is Number -> value.toString()
                    is Boolean -> value.toString()
                    null -> "null"
                    else -> serialize(value) // Recursive for nested objects
                }
                "\"$jsonName\": $jsonValue"
            }
            
        return "{${jsonFields.joinToString(", ")}}"
    }
}

data class Address(val street: String, val city: String)

data class Employee(
    @JsonName("emp_name") val name: String,
    val salary: Double,
    @JsonIgnore val internalId: String,
    val address: Address
)

fun main() {
    val emp = Employee(
        name = "John Doe",
        salary = 75000.0,
        internalId = "INTERNAL_123",
        address = Address("123 Main St", "Springfield")
    )
    
    println(SimpleJsonSerializer.serialize(emp))
}

Configuration System

import kotlin.reflect.full.*

@Target(AnnotationTarget.PROPERTY)
annotation class ConfigProperty(val key: String, val defaultValue: String = "")

@Target(AnnotationTarget.PROPERTY)
annotation class Required

class DatabaseConfig {
    @ConfigProperty("db.host", "localhost")
    var host: String = ""
    
    @ConfigProperty("db.port", "5432")  
    var port: Int = 0
    
    @ConfigProperty("db.username")
    @Required
    var username: String = ""
    
    @ConfigProperty("db.password")
    @Required  
    var password: String = ""
    
    @ConfigProperty("db.pool.size", "10")
    var poolSize: Int = 0
}

class ConfigLoader {
    fun load<T : Any>(configClass: KClass<T>, properties: Map<String, String>): T {
        val instance = configClass.java.getDeclaredConstructor().newInstance()
        
        configClass.memberProperties.forEach { property ->
            val configAnnotation = property.findAnnotation<ConfigProperty>()
            if (configAnnotation != null) {
                val value = properties[configAnnotation.key] ?: configAnnotation.defaultValue
                
                // Check if required
                if (property.hasAnnotation<Required>() && value.isEmpty()) {
                    throw IllegalStateException("Required property ${configAnnotation.key} is missing")
                }
                
                // Set value with type conversion
                if (property is kotlin.reflect.KMutableProperty1 && value.isNotEmpty()) {
                    val convertedValue = when (property.returnType.classifier) {
                        String::class -> value
                        Int::class -> value.toInt()
                        Double::class -> value.toDouble()
                        Boolean::class -> value.toBoolean()
                        else -> value
                    }
                    property.set(instance, convertedValue)
                }
            }
        }
        
        return instance
    }
}

fun main() {
    val properties = mapOf(
        "db.host" to "prod-db.company.com",
        "db.username" to "app_user",
        "db.password" to "secret123",
        "db.pool.size" to "20"
    )
    
    val loader = ConfigLoader()
    val config = loader.load(DatabaseConfig::class, properties)
    
    println("Database Config:")
    println("Host: ${config.host}")
    println("Port: ${config.port}")
    println("Username: ${config.username}")
    println("Pool Size: ${config.poolSize}")
}

Dependency Injection Container

import kotlin.reflect.KClass
import kotlin.reflect.full.*

@Target(AnnotationTarget.CONSTRUCTOR)
annotation class Inject

@Target(AnnotationTarget.CLASS)
annotation class Singleton

interface Logger {
    fun log(message: String)
}

@Singleton
class ConsoleLogger : Logger {
    override fun log(message: String) {
        println("[LOG] $message")
    }
}

interface UserRepository {
    fun findUser(id: String): String?
}

class DatabaseUserRepository @Inject constructor(
    private val logger: Logger
) : UserRepository {
    override fun findUser(id: String): String? {
        logger.log("Finding user with id: $id")
        return "User_$id"
    }
}

class SimpleContainer {
    private val singletons = mutableMapOf<KClass<*>, Any>()
    private val bindings = mutableMapOf<KClass<*>, KClass<*>>()
    
    fun <T : Any> bind(interfaceClass: KClass<T>, implementationClass: KClass<out T>) {
        bindings[interfaceClass] = implementationClass
    }
    
    fun <T : Any> get(clazz: KClass<T>): T {
        // Check if singleton already exists
        if (singletons.containsKey(clazz)) {
            @Suppress("UNCHECKED_CAST")
            return singletons[clazz] as T
        }
        
        // Find implementation class
        val implementationClass = bindings[clazz] ?: clazz
        
        // Find injectable constructor
        val constructor = implementationClass.constructors.find { 
            it.hasAnnotation<Inject>() 
        } ?: implementationClass.constructors.first()
        
        // Resolve dependencies
        val args = constructor.parameters.map { param ->
            @Suppress("UNCHECKED_CAST")
            get(param.type.classifier as KClass<Any>)
        }
        
        // Create instance
        val instance = constructor.call(*args.toTypedArray())
        
        // Store singleton if annotated
        if (implementationClass.hasAnnotation<Singleton>()) {
            singletons[clazz] = instance
        }
        
        @Suppress("UNCHECKED_CAST")
        return instance as T
    }
}

fun main() {
    val container = SimpleContainer()
    
    // Configure bindings
    container.bind(Logger::class, ConsoleLogger::class)
    container.bind(UserRepository::class, DatabaseUserRepository::class)
    
    // Resolve dependencies
    val userRepo = container.get(UserRepository::class)
    val result = userRepo.findUser("123")
    
    println("Result: $result")
    
    // Verify singleton behavior
    val logger1 = container.get(Logger::class)
    val logger2 = container.get(Logger::class)
    println("Same logger instance: ${logger1 === logger2}")
}

Common Pitfalls

  • Performance: Reflection is slower than direct access; cache reflection objects when possible
  • Security: Reflection can access private members; be cautious with user input
  • Type Safety: Reflection operations are not type-safe at compile time; use carefully
  • Proguard/R8: Code obfuscation can break reflection; configure keep rules appropriately

Best Practices

  • Use inline functions with reified type parameters when possible instead of reflection
  • Cache KClass and KFunction objects to avoid repeated reflection calls
  • Prefer sealed classes and when expressions over reflection for type checking
  • Use annotations with retention RUNTIME only when necessary
  • Consider compile-time code generation alternatives like KSP (Kotlin Symbol Processing)

Practice Exercises

  1. Create a validation framework that uses annotations to validate object properties
  2. Build a simple ORM that maps objects to SQL queries using reflection
  3. Implement a command pattern dispatcher that routes method calls based on annotations

Architecture Notes

  • Framework Design: Reflection enables building flexible frameworks and libraries
  • Configuration: Useful for dynamic configuration loading and dependency injection
  • Serialization: Powers JSON/XML serialization libraries
  • Testing: Enables accessing private members for testing purposes