Search code examples
kotlinreflection

get all annotated member properties in kotlin


I'm trying to get all member properties' JsonPath in kotlin.
Following is my data class with annotated fields

class Test(
    @Mask val testField: String?,
    val nested: NestedTest,
    val nestedList: List<NestedTest>?,
    val generic: GenericTest<NestedGenericTest>
)

class NestedTest(
    @Mask val test2: String = "",
    val nestedNestedTest: NestedNestedTest = NestedNestedTest()
)

class NestedNestedTest(
    @Mask
    val test3: String = ""
)

class GenericTest<T>(
    val generic: T
)

class NestedGenericTest(
    @Mask
    val nestedGeneric: String
)

H'm trying using kotlin-reflection, but I don't care what I use.
how can I get it?


Solution

  • Okay, so let's start with defining our annotation.

    @Retention(AnnotationRetention.RUNTIME)
    annotation class Mask
    

    The important thing here is the retention, which must be runtime.

    In my solution I am going to operate on Kotlin properties, so I need to slightly change the Annotation target:

    // from
    class Clazz(
        @Mask val field: Field
    )
    // to
    class Clazz(
        @property:Mask val field: Field
    )
    

    What I need to do is to take all object properties and iterate over them (to find all marked fields).

    At first lest define some types I am not going to get deeper. In my case, it's just a list of primitives and String:

    val simpleTypes = setOf(
        // define types you would like to stop iterating (like "basic types")
        Byte::class,
        Boolean::class,
        Short::class,
        Integer::class,
        Long::class,
        Float::class,
        Double::class,
        Char::class,
        String::class,
    )
    

    So, in my function, I am going to return a property definition with its value:

    typealias KPropertyWithValue = Pair<KProperty1<out Any, *>, Any?>
    

    To finally get my functions:

    fun getMaskedFromObject(v: Any?): List<KPropertyWithValue> {
        // early returns - support for some special types, like Collection, Arrays etc.
        when {
            v == null -> return emptyList()
            v is Collection<*> -> return v.flatMap { getMaskedFromObject(it) }
            v is Array<*> -> return v.flatMap { getMaskedFromObject(it) }
            // support for other types (Streams, custom collections etc.)
            v::class in simpleTypes -> return emptyList()
        }
    
        v!! // just a mark the v is not null at this point - kotlin compiler is not handling the first when/return properly
    
        val properties = v::class.memberProperties
        val annotated = properties.filter { it.annotations.any { ann -> ann is Mask } }
        val propertyValues = properties.map { it.getter.call(v) }
    
        val annotatedWithValue = annotated.map {
            val value = it.getter.call(v)
            Pair(it, value)
        }
    
        return annotatedWithValue + propertyValues.flatMap { getMaskedFromObject(it) }
    }
    

    So, with a code like:

    fun main() {
        val test = Test(
            testField = "root_testField",
            nested = NestedTest(
                test2 = "nested_test2",
                nestedNestedTest = NestedNestedTest(
                    test3 = "nested_test3"
                )
            ),
            nestedList = listOf(
                NestedTest(
                    test2 = "list_test2",
                    nestedNestedTest = NestedNestedTest(
                        test3 = "list_test3"
                    )
                )
            ),
            generic = GenericTest(
                generic = NestedGenericTest(
                    nestedGeneric = "generic_nested"
                )
            )
        )
    
        val result = getMaskedFromObject(test)
        result.forEach { 
            println(it)
        }
    }
    

    The result is:

    (val com.example.springsandbox.utils.Test.testField: kotlin.String?, root_testField)
    (val com.example.springsandbox.utils.NestedGenericTest.nestedGeneric: kotlin.String, generic_nested)
    (val com.example.springsandbox.utils.NestedTest.test2: kotlin.String, nested_test2)
    (val com.example.springsandbox.utils.NestedNestedTest.test3: kotlin.String, nested_test3)
    (val com.example.springsandbox.utils.NestedTest.test2: kotlin.String, list_test2)
    (val com.example.springsandbox.utils.NestedNestedTest.test3: kotlin.String, list_test3)
    

    Please notice a few things:

    • there are a few types that would require additional coding (Arrays, maps, custom collections, Streams etc.)
    • we cannot determine if a value comes from the object (by nesting) or the collection, but it's possible to create such a "path" while processing the object
    • I am operating on Kotlin Properties, there will be a few differences if you would like to operate on Java fields