article banner

Kotlin Reflection: Class references

This is a chapter from the book Advanced Kotlin. You can find it on LeanPub or Amazon.

In the previous part, we covered the reflection references hierarchy, function references, and property references. This part is dedicated to class references and ends with a practical example of object serialization.

To reference a class, we use the class name or instance, the double colon, and the class keyword. The result is KClass<T>, where T is the type representing a class.

import kotlin.reflect.KClass class A fun main() { val class1: KClass<A> = A::class println(class1) // class A val a: A = A() val class2: KClass<out A> = a::class println(class2) // class A }

Note that the reference on the variable is covariant because a variable of type A might contain an object of type A or any of its subtypes.

import kotlin.reflect.KClass open class A class B : A() fun main() { val a: A = B() val clazz: KClass<out A> = a::class println(clazz) // class B }

Since class is a reserved keyword in Kotlin and cannot be used as the variable name, it is a popular practice to use "clazz" instead.

A class has two types of names:

  • Simple name is just the name used after the class keyword. We can read it using the simpleName property.
  • The fully qualified name is the name that includes the package and the enclosing classes. We can read it using the qualifiedName property.
package a.b.c class D { class E } fun main() { val clazz = D.E::class println(clazz.simpleName) // E println(clazz.qualifiedName) // a.b.c.D.E }

Both simpleName and qualifiedName return null when we reference an object expression or any other nameless object.

fun main() { val o = object {} val clazz = o::class println(clazz.simpleName) // null println(clazz.qualifiedName) // null }

KClass has only a few properties that let us check some class-specific characteristics:

  • isFinal: Boolean - true if this class is final.
  • isOpen: Boolean - true if this class has the open modifier. Abstract and sealed classes, even though they are generally considered abstract, will return false.
  • isAbstract: Boolean - true if this class has the abstract modifier. Sealed classes, even though they are generally considered abstract, will return false.
  • isSealed: Boolean - true if this class has the sealed modifier.
  • isData: Boolean - true if this class has the data modifier.
  • isInner: Boolean - true if this class has the inner modifier.
  • isCompanion: Boolean - true if this class is a companion object.
  • isFun: Boolean - true if this class is a Kotlin functional interface and so has the fun modifier.
  • isValue: Boolean - true if this class is a value class and so has the value modifier.

Just like for functions, we can check classes’ visibility using the visibility property.

sealed class UserMessages private data class UserId(val id: String) { companion object { val ZERO = UserId("") } } internal fun interface Filter<T> { fun check(value: T): Boolean } fun main() { println(UserMessages::class.visibility) // PUBLIC println(UserMessages::class.isSealed) // true println(UserMessages::class.isOpen) // false println(UserMessages::class.isFinal) // false println(UserMessages::class.isAbstract) // false println(UserId::class.visibility) // PRIVATE println(UserId::class.isData) // true println(UserId::class.isFinal) // true println(UserId.Companion::class.isCompanion) // true println(UserId.Companion::class.isInner) // false println(Filter::class.visibility) // INTERNAL println(Filter::class.isFun) // true }

Functions and properties defined inside a class are known as members. This category does not include extension functions defined outside the class, but it does include elements defined by parents. Members defined by a particular class are called declared members. Since referencing a class to list its members is quite popular, it is good to know the following properties we can use:

  • members: Collection<KCallable<*>> - returns all class members, including those declared by parents of this class.
  • functions: Collection<KFunction<*>> - returns all class member functions, including those declared by parents of this class.
  • memberProperties: Collection<KProperty1<*>> - returns all class member properties, including those declared by parents of this class.
  • declaredMembers: Collection<KCallable<*>> - returns members declared by this class.
  • declaredFunctions: Collection<KFunction<*>> - returns functions declared by this class.
  • declaredMemberProperties: Collection<KProperty1<*>> - returns member properties declared by this class.
import kotlin.reflect.full.* open class Parent { val a = 12 fun b() {} } class Child : Parent() { val c = 12 fun d() {} } fun Child.e() {} fun main() { println(Child::class.members.map { it.name }) // [c, d, a, b, equals, hashCode, toString] println(Child::class.functions.map { it.name }) // [d, b, equals, hashCode, toString] println(Child::class.memberProperties.map { it.name }) // [c, a] println(Child::class.declaredMembers.map { it.name }) // [c, d] println(Child::class.declaredFunctions.map { it.name }) // [d] println(Child::class.declaredMemberProperties.map { it.name }) // [c] }

A class constructor is not a member, but it is not considered a function either. We can get a list of all constructors using the constructors property.

package playground import kotlin.reflect.KFunction class User(val name: String) { constructor(user: User) : this(user.name) constructor(json: UserJson) : this(json.name) } class UserJson(val name: String) fun main() { val constructors: Collection<KFunction<User>> = User::class.constructors println(constructors.size) // 3 constructors.forEach(::println) // fun <init>(playground.User): playground.User // fun <init>(playground.UserJson): playground.User // fun <init>(kotlin.String): playground.User }

We can get superclass references using the superclasses property, which returns List<KClass<*>>. In reflection API nomenclature, remember that interfaces are also considered classes, so their references are of type KClass and they are returned by the superclasses property. We can also get the types of the same direct superclass and directly implemented interfaces using the supertypes property, which returns List<KType>. This property actually returns a list of superclasses, not supertypes, as it doesn’t include nullable types, but it includes Any if there is no other direct superclass.

import kotlin.reflect.KClass import kotlin.reflect.full.superclasses interface I1 interface I2 open class A : I1 class B : A(), I2 fun main() { val a = A::class val b = B::class println(a.superclasses) // [class I1, class kotlin.Any] println(b.superclasses) // [class A, class I2] println(a.supertypes) // [I1, kotlin.Any] println(b.supertypes) // [A, I2] }

You can use a class reference to check if a specific object is a subtype of this class (or interface).

interface I1 interface I2 open class A : I1 class B : A(), I2 fun main() { val a = A() val b = B() println(A::class.isInstance(a)) // true println(B::class.isInstance(a)) // false println(I1::class.isInstance(a)) // true println(I2::class.isInstance(a)) // false println(A::class.isInstance(b)) // true println(B::class.isInstance(b)) // true println(I1::class.isInstance(b)) // true println(I2::class.isInstance(b)) // true }

Generic classes have type parameters that are represented with the KTypeParameter type. We can get a list of all type parameters defined by a class using the typeParameters property.

fun main() { println(List::class.typeParameters) // [out E] println(Map::class.typeParameters) // [K, out V] }

If a class includes some nested classes, we can get a list of them using the nestedClasses property.

class A { class B inner class C } fun main() { println(A::class.nestedClasses) // [class A$B, class A$C] }

If a class is a sealed class, we can get a list of its subclasses using sealedSubclasses: List<KClass<out T>>.

sealed class LinkedList<out T> class Node<out T>( val head: T, val next: LinkedList<T> ) : LinkedList<T>() object Empty : LinkedList<Nothing>() fun main() { println(LinkedList::class.sealedSubclasses) // [class Node, class Empty] }

An object declaration has only one instance, and we can get its reference using the objectInstance property of type T?, where T is the KClass type parameter. This property returns null when a class does not represent an object declaration.

import kotlin.reflect.KClass sealed class LinkedList<out T> data class Node<out T>( val head: T, val next: LinkedList<T> ) : LinkedList<T>() data object Empty : LinkedList<Nothing>() fun main() { println(Node::class.objectInstance) // null println(Empty::class.objectInstance) // Empty }

Serialization example

Let's use our knowledge now on a practical example. Our goal is to define a toJson function which will serialize objects into JSON format.

class Creature( val name: String, val attack: Int, val defence: Int, ) fun main() { val creature = Creature( name = "Cockatrice", attack = 2, defence = 4 ) println(creature.toJson()) // {"attack": 2, "defence": 4, "name": "Cockatrice"} }

To help us implement toJson, I will define a couple of helper functions, starting with objectToJson, which is responsible for serializing objects to JSON and assumes that its argument is an object. Objects in JSON format are surrounded by curly braces containing property-value pairs separated with commas. In each pair, first there is a property name in quotes, then a colon, and then a serialized value. To implement objectToJson, we first need to have a list of object properties. For that, we will reference this object, and then we can either use memberProperties (including all properties in this object, including those inherited from the parent) or declaredMemberProperties (including properties declared by the class constructing this object). Once we have a list of properties, we can use joinToString to create a string with property-value pairs. We specify prefix and postfix parameters to surround the result string with curly brackets. We also define transform to specify how property-value pairs should be transformed to a string. Inside them, we take property names using the name property; we get property value by calling the call method from this property reference, and we then transform the result value to a string using the valueToJson function.

fun Any.toJson(): String = objectToJson(this) private fun objectToJson(any: Any) = any::class .memberProperties .joinToString( prefix = "{", postfix = "}", transform = { prop -> "\"${prop.name}\": ${valueToJson(prop.call(any))}" } )

The above code needs the 'valueToJsonfunction to serialize JSON values. JSON format supports a number of values, but most of them can just be serialized using the Kotlin string template. This includes thenullvalue, all numbers, and enums. An important exception is strings, which need to be additionally wrapped with quotes[^9_2]. All non-basic types will be treated as objects and serialized with theobjectToJson` function.

private fun valueToJson(value: Any?): String = when (value) { null, is Number -> "$value" is String, is Enum<*> -> "\"$value\"" // ... else -> objectToJson(value) }

This is all we need to make a simple JSON serialization function. To make it more functional, I also added some methods to serialize collections.

import kotlin.reflect.full.memberProperties // Serialization function definition fun Any.toJson(): String = objectToJson(this) private fun objectToJson(any: Any) = any::class .memberProperties .joinToString( prefix = "{", postfix = "}", transform = { prop -> "\"${prop.name}\": ${valueToJson(prop.call(any))}" } ) private fun valueToJson(value: Any?): String = when (value) { null, is Number, is Boolean -> "$value" is String, is Enum<*> -> "\"$value\"" is Iterable<*> -> iterableToJson(value) is Map<*, *> -> mapToJson(value) else -> objectToJson(value) } private fun iterableToJson(any: Iterable<*>): String = any .joinToString( prefix = "[", postfix = "]", transform = ::valueToJson ) private fun mapToJson(any: Map<*, *>) = any.toList() .joinToString( prefix = "{", postfix = "}", transform = { "\"${it.first}\": ${valueToJson(it.second)}" } ) // Example use class Creature( val name: String, val attack: Int, val defence: Int, val traits: List<Trait>, val cost: Map<Element, Int> ) enum class Element { FOREST, ANY, } enum class Trait { FLYING } fun main() { val creature = Creature( name = "Cockatrice", attack = 2, defence = 4, traits = listOf(Trait.FLYING), cost = mapOf( Element.ANY to 3, Element.FOREST to 2 ) ) println(creature.toJson()) // {"attack": 2, "cost": {"ANY": 3, "FOREST": 2}, // "defence": 4, "name": "Cockatrice", // "traits": ["FLYING"]} }

Before we close this topic, we might also practice working with annotations. We will define the JsonName annotation, which should set a different name for the serialized form, and JsonIgnore, which should make the serializer ignore the annotated property.

// Annotations @Target(AnnotationTarget.PROPERTY) annotation class JsonName(val name: String) @Target(AnnotationTarget.PROPERTY) annotation class JsonIgnore // Example use class Creature( @JsonIgnore val name: String, @JsonName("att") val attack: Int, @JsonName("def") val defence: Int, val traits: List<Trait>, val cost: Map<Element, Int> ) enum class Element { FOREST, ANY, } enum class Trait { FLYING } fun main() { val creature = Creature( name = "Cockatrice", attack = 2, defence = 4, traits = listOf(Trait.FLYING), cost = mapOf( Element.ANY to 3, Element.FOREST to 2 ) ) println(creature.toJson()) // {"att": 2, "cost": {"ANY": 3, "FOREST": 2}, // "def": 4, "traits": ["FLYING"]} }

To respect these annotations, we need to modify our objectToJson function. To ignore properties, we will add a filter on the properties list. For each property, we need to check if it has the JsonIgnore annotation. To check if a property has this annotation, we could use the annotations property, but we can also use the hasAnnotation extension function on KAnnotatedElement. To respect a name change, we need to find the JsonName property annotation by using the findAnnotation extension function on KAnnotatedElement. This is how our function needs to be modified to respect both annotations:

private fun objectToJson(any: Any) = any::class .memberProperties .filterNot { it.hasAnnotation<JsonIgnore>() } .joinToString( prefix = "{", postfix = "}", transform = { prop -> val annotation = prop.findAnnotation<JsonName>() val name = annotation?.name ?: prop.name "\"${name}\": ${valueToJson(prop.call(any))}" } )

In the next part, we’ll cover referencing types and see how to implement a function to generate an example value for a specified type.

2:

In fact, strings are much harder to serialize because special characters need to be escaped so as not to mess with the JSON format, but I will ignore this to keep this example simple.