Metaprogramming in Kotlin: How to Make Code Write Code
\n\nImagine 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\nIn 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\nReflection in Kotlin: Peeking Under the Hood
\n\nReflection 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.
Example: suppose you need to serialize an object to JSON without knowing its type in advance. Reflection allows you to do this:
\n\nimport 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\nThis 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\nInline Functions and Reified Types
\n\nOne 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\nSee how elegantly you can get the type at runtime:
\n\ninline 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\nWithout 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.
Annotations: Your Personal DSL for Metadata
\n\nAnnotations 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