🖌 The Guide To Your First Annotation Processor with KSP (And Becoming A Kotlin Artist)
In this article, we are going to create a KSP-based annotation processor that generates new code and files based on annotation usages. If you’d like to know more about code generation and make your development process more productive and fun, continue reading!
(Also, if you want a TL;DR version, you can visit this repository on GitHub for the completed code and library)
What Is KSP?
KSP is an API that gives the ability to Kotlin developers to develop light-weight compiler plugins that analyze and generate code while keeping them away from unnecessary complexities of writing an actual compiler plugin.
Many libraries (including Room) are currently using KSP, instead of KAPT. You can find a list of few of them here.
How Does KSP Work?
In Kotlin developers have access to compiler APIs which they can use to develop compiler plugins. These plugins have access to almost all parts of the compilation process and can modify the inputs of the program.
Writing compiler plugins for simple code generation might get complex and that’s why KSP has been created. It is a compiler plugin under the hood which hides away the complexity (and the dependency to the compiler itself) of writing a compiler plugin by maintaining a simple API.
Bear in mind that unlike compiler plugins, KSP cannot modify the compiled code and treats them as read-only inputs.
Comparison to KAPT
KAPT is a Java-based Annotation Processor, which is tied to the JVM, while KSP is a code processor which depends only on Kotlin and can be more natural to Kotlin developers.
On another note, KAPT needs to have access to Java generated stubs to modify the program input based on annotations. This stage happens after all the symbols in the program (such as types) are completely resolved. This stage takes 30% of the compiler time and can be costly.
Since not all code generators need all of the symbol resolution (as it is the case in our example), KSP can be much faster since it happens in an earlier stage of the compiler, which not all (but enough) symbols are completely resolved.
Developing Your First KSP Project
In this article, we are going to create ListGen, a KSP-based library that creates a list out of all the functions that have a specific annotation.
For example, you can add @Listed
Annotation to your functions:
// can be anywhere (or in any module) in your project
@Listed("mainList")
fun mainModule() = 2
// can be anywhere (or in any module) in your project
@Listed("otherList")
fun helloModule() = "hello!"
// can be anywhere (or in any module) in your project
@Listed("mainList")
fun secondModule() = 3
And have a list of them generated like below:
// in build/.../GeneratedLists.kt
val mainList = listOf(mainModule(), secondModule())
val otherList = listOf(helloModule())
So let’s get generating!
To get started with KSP, you only needs a few things. To get started, add the KSP API to your KSP-module dependencies:
Creating The Annotation Class
Since we want to use KSP for annotation processing, we need to define our custom annotation(s) so we can use them later on. In this case, we want to have an annotation called @Listed
that takes a name as an input and is defined on functions. Later we will create a list of all their usages, using the provided names.
Creating The Processor And Its Provider
In order to process files (and create more of them) yo need to create a SymbolProcessor and introduce it to KSP by using a SymbolProcessorProvider (which is basically a factory for the processor), since on the JVM, KSP uses a Service Loader to find the provider.
For now, we won’t process anything to complete our setup.
In order to introduce the provider to the JVM’s ServiceLoader, we need to create the following file.
In order to use this generator (which currently does nothing) we need to declare a KSP dependency to its module, in our application module.
We’re all set. Now we can start developing our processor.
Crafting The Processor
In order to create the processor, you need to ask yourself: “What is this processor supposed to generate?”. To answer that question, you can start by doing things by hand and start creating files and codes that are supposed to the generated. This way you get a lot of insight into what actually needs to be happening inside your processor.
In our case, we want to generate a file that looks like the following and has the proper imports:
val mainList = listOf(mainModule(), secondModule())
val otherList = listOf(helloModule())
In order to do this, we need to
- Add a proper package to the generated file
- Find mainModule, secondModule and helloModule
- Know where they are and add the proper imports for them above the file
- Find their name (mainList and otherList) from the annotation
- Generate a file containing the information above
- Make it efficient
Finding Annotations
KSP provides a resolver which you can use to find every symbol in the processed module that has a specific annotation.
Let’s generate a file containing a comment with the names of the functions that have the @Listed
annotation.

The process function needs to return the symbols that were not valid. KSP uses this information for its multiple round processing.
As you can see, creating a file and filling the information is a piece of cake. All that is remaining is to add imports, read annotations’ names and add the functions and we are good to go.

We’re all done. You can continue to clean up the file and add more features if necessary. You can also add unit tests (you can see my unit tests here).
Note: In this example we used a simple StringBuilder to create the contents of the file. For more advanced usages you can use libraries like KotlinPoet to write the contents of the generated files more efficiently.
Using the visitor pattern
For the basic example above, we have filtered the symbols on the function types and iterated over them, since that was the only symbol that we needed. If you need to support more symbols (like classes), you can also use a KSVisitor and pass it to your symbols’s accept function, which will call the proper function on your visitor (e.g. visitFunctionDeclaration is called if your symbol is a function). You can see it in action here.
Performance
To make the processor super fast, a few things need to be considered.
Minimizing the amount of processed files
Operate on as few as possible files to get the desired results. Notice the above code that operates only on the functions that have our specific annotations. All other symbols are ignored.
Informing the compiler
KSP is smart and has an incremental compilation strategy. We don’t want our generated file(s) to change, unless:
- A previously existing file that contained our annotations has been changed/deleted.
- A new file containing our annotation has been added
All other files should be ignored.
In order to achieve this, we have given our dependencies to the createNewFile function, which informs the processor about the files that we have considered for creating this file.
Avoiding expensive functions
Some functions in KSP APIs are expensive (as they are noted in their documentation). One example is resolve, which resolves a TypeReference to a Type. These functions are expensive and should be used only if knowledge about them is absolutely necessary. Try to look out for these functions (and their documentation) and use them sparingly.
Conclusion
KSP is a powerful tool that helps developers to write light-weight compiler plugins and annotation processors while maintaining a Kotlin-friendly API.
Using KSP can help developers and tech leaders to create libraries that help them achieve more productivity by generating files and boilerplate code.
I hope you enjoyed this article and it has helped you learn how to create your first KSP library.
If you enjoyed this article, be sure to check out my other articles:
🔒 Synchronization, Thread-Safety and Locking Techniques in Java and Kotlin
💥 The Story of My First A-ha Moment With Jetpack Compose | by Adib Faramarzi | ProAndroidDev
Kotlin Contracts: Make Great Deals With The Compiler! 🤜🤛
Follow me on Medium If you’re interested in more informative and in-depth articles about Java, Kotlin and Android. You can also ping me on Twitter @TheSNAKY 🍻.