Kotlin Reflection: Class references
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.
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.
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 thesimpleName
property. - The fully qualified name is the name that includes the package and the enclosing classes. We can read it using the
qualifiedName
property.
Both simpleName
and qualifiedName
return null
when we reference an object expression or any other nameless object.
KClass
has only a few properties that let us check some class-specific characteristics:
isFinal: Boolean
-true
if this class isfinal
.isOpen: Boolean
-true
if this class has theopen
modifier. Abstract and sealed classes, even though they are generally considered abstract, will returnfalse
.isAbstract: Boolean
-true
if this class has theabstract
modifier. Sealed classes, even though they are generally considered abstract, will returnfalse
.isSealed: Boolean
-true
if this class has thesealed
modifier.isData: Boolean
-true
if this class has thedata
modifier.isInner: Boolean
-true
if this class has theinner
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 thefun
modifier.isValue: Boolean
-true
if this class is a value class and so has thevalue
modifier.
Just like for functions, we can check classes’ visibility using the visibility
property.
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.
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.
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.
You can use a class reference to check if a specific object is a subtype of this class (or interface).
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.
If a class includes some nested classes, we can get a list of them using the nestedClasses
property.
If a class is a sealed class, we can get a list of its subclasses using sealedSubclasses: List<KClass<out T>>
.
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.
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.
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 quotes2. All non-basic types will be treated as objects and serialized with the objectToJson
function.
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.
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.
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:
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.
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.