Metaprogramming in Kotlin: From Reflection to Code Generation

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Metaprogramming in Kotlin: How to Make Code Write Code

\n\n

Imagine writing code that writes code itself. Sounds like science fiction? In the world of metaprogramming in Kotlin, this is a reality. Metaprogramming is a technique where a program manipulates itself or other programs as data. For a Kotlin developer, this is a superpower that allows you to reduce tons of boilerplate code, create flexible frameworks, and automate routine tasks.

\n\n

In this article, we will explore advanced metaprogramming techniques in Kotlin: from classic reflection to modern code generation with KSP. You will learn how to use reified types, inline functions, annotations, and much more. Let's go!

\n\n

Reflection in Kotlin: Peeking Under the Hood

\n\n

Reflection is the foundation of metaprogramming. In Kotlin, it is implemented through the kotlin-reflect library. With it, you can dynamically obtain information about classes, functions, and properties at runtime.

\n\n

Example: suppose you need to serialize an object to JSON without knowing its type in advance. Reflection allows you to do this:

\n\n
import kotlin.reflect.full.memberProperties\n\nfun toJson(obj: Any): String {\n    val sb = StringBuilder("{")\n    val props = obj::class.memberProperties\n    props.forEachIndexed { index, prop ->\n        sb.append("\\"${prop.name}\\": \\"${prop.get(obj)}\\"")\n        if (index < props.size - 1) sb.append(", ")\n    }\n    sb.append("}")\n    return sb.toString()\n}\n\ndata class User(val name: String, val age: Int)\n\nfun main() {\n    val user = User("Alice", 30)\n    println(toJson(user)) // {"name": "Alice", "age": "30"}\n}\n
\n\n

This is a basic but powerful technique. However, reflection has a drawback — performance. For high-load systems, it is better to use code generation, which we will discuss later.

\n\n

Inline Functions and Reified Types

\n\n

One of the coolest features of Kotlin for metaprogramming is reified types. They only work inside inline functions and allow you to preserve type information at runtime, which is usually impossible due to type erasure on the JVM.

\n\n

See how elegantly you can get the type at runtime:

\n\n
inline fun <reified T> typeName(): String {\n    return T::class.simpleName ?: "Unknown"\n}\n\nfun main() {\n    println(typeName<String>())   // String\n    println(typeName<List<Int>>()) // List\n}\n
\n\n

Without reified, this trick won't work — the compiler will simply erase the type. But with this technique, you can create typed factories, serializers, and even entire frameworks. This is a true reified type in Kotlin in action.

\n\n

Annotations: Your Personal DSL for Metadata

\n\n

Annotations in Kotlin are not just markers. In combination with reflection or code generation, they become a powerful metaprogramming tool. Let's create our own annotation for validation:

\n\n
@Target(AnnotationTarget.FIELD)\n@Retention(AnnotationRetention.RUNTIME)\nannotation class NotBlank(val message: String = "Field must not be blank")\n\nfun validate(obj: Any): List<String> {\n    val errors = mutableListOf<String>()\n    obj::class.memberProperties.forEach { prop ->\n        val annotation = prop.findAnnotation<NotBlank>()\n        if (annotation != null) {\n            val value = prop.get(obj) as? String\n            if (value.isNullOrBlank()) {\n                errors.add("${prop.name}: ${annotation.message}")\n            }\n        }\n    }\n    return errors\n}\n\ndata class Form(val name: String, @NotBlank val email: String)\n\nfun main() {\n    val form = Form("Ivan", "")\n    println(validate(form)) // [email: Field must not be blank]\n}\n                

Blogs

Book Recommendations