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.
The above code needs the valueToJson function 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 the null value, 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 the objectToJson 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.
[^9_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.
Marcin Moskala is a highly experienced developer and Kotlin instructor as the founder of Kt. Academy, an official JetBrains partner specializing in Kotlin training, Google Developers Expert, known for his significant contributions to the Kotlin community. Moskala is the author of several widely recognized books, including "Effective Kotlin," "Kotlin Coroutines," "Functional Kotlin," "Advanced Kotlin," "Kotlin Essentials," and "Android Development with Kotlin."
Beyond his literary achievements, Moskala is the author of the largest Medium publication dedicated to Kotlin. As a respected speaker, he has been invited to share his insights at numerous programming conferences, including events such as Droidcon and the prestigious Kotlin Conf, the premier conference dedicated to the Kotlin programming language.
Owen has been developing software since the mid 1990s and remembers the productivity of languages such as Clipper and Borland Delphi.
Since 2001, He moved to Web, Server based Java and the Open Source revolution.
With many years of commercial Java experience, He picked up on Kotlin in early 2015.
After taking detours into Clojure and Scala, like Goldilocks, He thinks Kotlin is just right and tastes the best.
Owen enthusiastically helps Kotlin developers continue to succeed.