Avalon A Programmer Blog

Access annotation fields by reflection in Kotlin

This only works on Kotlin 1.1.4 or above. There is a bug exist before that.

Annotations and reflection always come in handy when you try to avoid boilerplate. Today I am going to get all the fields marked by a custom annotation, and put its value into a map.

Before you start, you need to include the reflection library from Kotlin. It depends on your build system. I will use Gradle as an example.

compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"

Create annotation

First, you need to create your own annotation class.

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

For target, you need to choose AnnotationTarget.PROPERTY over AnnotationTarget.FIELD because AnnotationTarget.FIELD does not support getter.

As you are going to use reflection, the retention is needed to set as AnnotationRetention.RUNTIME, so you can access the annotation through reflection.

The name property is for specify the key in the result map. Default is empty. If the name is empty, we will fallback to the property name.

Reflection

There are two way to approach this problem. We can use Data::class.java or Data::class. The former one is from Java, you will manipulate the Class<?>. The latter one is from Kotlin, you will manipulate the KClass<*>. In the following code, I will use the latter one because the former one require many extra works to work with getter.

Helper

I wrote a helper function to help access the value.

internal fun <T, R> KProperty1<T, R>.getValue(from: Any, forced: Boolean = true): Any? {
    if (forced)
        isAccessible = true
    javaField?.let { return it.get(from) }
    javaGetter?.let { return it.invoke(from) }
    return null
}

Setting isAccessible to true is need if you want to access private field. Depends on your runtime environment, it may throws exception. Either javaField or javaGetter is need to access the value.

val field1 = 1 // access by javaField
val field2     // access by javaGetter
    get = 2

It should never reach the last line. You can choose to throw an exception if you desire.

Implementation

internal fun getAllFields(from: Any): Map<String, Any?> {
    val map = mutableMapOf<String, Any?>()
    from::class.memberProperties.forEach{
        val annotation = it.findAnnotation<Field>() ?: return@forEach
        // Use annotation name if available. Fallback to field name.
        val name = if (annotation.name.isNotBlank()) annotation.name else it.name
        it.getValue(from).let {
            map.put(name, it)
        }
    }
    return map
}

First, we loop properties of a object and find the one marked with annotation. Then, set the key by annotation or fallback to field name. Finally, we put the value into the map.

However, it has a little problem that the inherent properties are not included. This is because memberProperties only return the properties of that class. We modify the code above to include parent.

internal fun getAllFields(from: Any): Map<String, Any?> {
    val classes = mutableListOf(from::class)
    classes.addAll(from::class.superclasses)
    return getAllFieldFromClass(from, classes)
}

private fun getAllFieldFromClass(instance: Any, classes: List<KClass<*>>): Map<String, Any?> {
    val map = mutableMapOf<String, Any?>()
    classes.forEach {
        it.memberProperties.forEach memberLoop@ {
            val annotation = it.findAnnotation<Field>() ?: return@memberLoop
            // Use annotation name if available. Fallback to field name.
            val name = if (annotation.name.isNotBlank()) annotation.name else it.name
            it.getValue(instance).let {
                map.put(name, it)
            }
        }
    }
    return map
}

Note that the field with the same name can be overwritten. If you care about that, you can change it to putIfAbsent instead of put or change the order of the list.