KSP & Annotation Processing
Reviewed & published by Brayan K
Kotlin Symbol Processing (KSP) is the modern way to generate code at compile time from annotations — the engine behind Room, Hilt, and Moshi — replacing the slower, Java-stub-based KAPT.
Part of the free Kotlin course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
By the end you'll understand how KSP works, how it differs from KAPT, and how a SymbolProcessor generates sources.
What You'll Learn in This Lesson
- What compile-time code generation is
- KSP vs KAPT and why KSP is faster
- SymbolProcessor and SymbolProcessorProvider
- How Room, Hilt and Moshi use KSP
- The ksp(...) Gradle configuration
- Where generated sources end up
1️⃣ What Compile-Time Code Generation Is
You add an annotation; a processor reads it during compilation and writes new Kotlin source. The generated code is compiled with yours — no runtime reflection, full type safety.
// You annotate your code...
annotation class GenerateBuilder
@GenerateBuilder
data class User(val name: String, val email: String)
// ...and a KSP processor generates new source at COMPILE TIME, e.g.:
// class UserBuilder {
// var name: String = ""
// var email: String = ""
// fun build() = User(name, email)
// }
// You never write UserBuilder by hand — KSP emits it for you.This is exactly how Room turns a @Dao interface into a working implementation behind the scenes.
2️⃣ Wiring KSP in Gradle
Apply the KSP plugin, then register each processor with the ksp(...) configuration — the faster, Kotlin-native replacement for kapt(...).
// build.gradle.kts — apply the KSP plugin and add a processor.
plugins {
kotlin("jvm")
id("com.google.devtools.ksp") version "2.0.0-1.0.21"
}
dependencies {
implementation("com.squareup.moshi:moshi:1.15.1")
// 'ksp(...)' registers the symbol processor (here Moshi's codegen).
ksp("com.squareup.moshi:moshi-kotlin-codegen:1.15.1")
}KSP understands Kotlin directly instead of generating Java stubs, which is why it builds significantly faster than KAPT.
3️⃣ Inside a SymbolProcessor
A SymbolProcessorProvider is the entry point that creates your SymbolProcessor. Its process() method uses a Resolver to find annotated symbols and a CodeGenerator to emit files.
import com.google.devtools.ksp.processing.*
import com.google.devtools.ksp.symbol.KSAnnotated
// 1) The PROVIDER is the entry point KSP discovers; it builds your processor.
class BuilderProcessorProvider : SymbolProcessorProvider {
override fun create(env: SymbolProcessorEnvironment): SymbolProcessor =
BuilderProcessor(env.codeGenerator, env.logger)
}
// 2) The PROCESSOR inspects symbols and generates code.
class BuilderProcessor(
private val codeGenerator: CodeGenerator,
private val logger: KSPLogger,
) : SymbolProcessor {
override fun process(resolver: Resolver): List<KSAnnotated> {
// Find everything annotated with @GenerateBuilder...
val symbols = resolver
.getSymbolsWithAnnotation("com.example.GenerateBuilder")
// ...then use codeGenerator to write new .kt files. (omitted)
return emptyList()
}
}Your turn. Replace the TODOs, then compare.
// 🎯 YOUR TURN — replace each TODO in build.gradle.kts.
plugins {
kotlin("jvm")
// 1) Apply the KSP Gradle plugin.
TODO // 👉 id("com.google.devtools.ksp") version "2.0.0-1.0.21"
}
dependencies {
implementation("androidx.room:room-runtime:2.6.1")
// 2) Register Room's annotation processor via KSP.
TODO("androidx.room:room-compiler:2.6.1") // 👉 ksp(...)
}Mini-Challenge: Sketch a Processor
Write a provider and processor that finds annotated symbols and logs how many it found.
// 🎯 MINI-CHALLENGE: sketch a SymbolProcessor.
//
// 1. Write a SymbolProcessorProvider whose create() returns
// your processor, passing env.codeGenerator and env.logger.
// 2. Write the SymbolProcessor. In process(resolver):
// - call resolver.getSymbolsWithAnnotation("com.example.AutoToString")
// - log how many symbols you found with logger.info(...)
// - return emptyList()
// 3. (Conceptually) for each symbol, you'd use codeGenerator to
// emit a generated toString() helper file.
//
// ✅ During the build, KSP runs your processor and logs the count.
import com.google.devtools.ksp.processing.*
import com.google.devtools.ksp.symbol.KSAnnotated
// your code hereCommon Errors (and the fix)
- Using kapt(...) for a KSP processor — Apply the KSP plugin and switch to the ksp(...) configuration.
- Forgetting the KSP Gradle plugin — Without id("com.google.devtools.ksp"), the ksp(...) configuration won't exist.
- "Generated class not found" — Build once so KSP runs; generated sources live under build/generated.
- Version mismatch — The KSP plugin version is tied to a Kotlin version; keep them aligned.
Pro Tips
- 💡 Prefer KSP over KAPT wherever a library offers it — your builds get noticeably faster.
- 💡 Code generation beats reflection for performance and compile-time safety.
- 💡 Peek at the generated sources under build/generated/ksp when debugging.
📋 Quick Reference — KSP
| Concept | KSP API |
|---|---|
| Apply the plugin | id("com.google.devtools.ksp") |
| Register a processor | ksp("group:processor:1.0") |
| Entry point | SymbolProcessorProvider |
| Do the work | SymbolProcessor.process(...) |
| Query symbols | resolver.getSymbolsWithAnnotation(...) |
| Generated output | build/generated/ksp |
🎉 Lesson Complete!
- ✅ KSP generates code at compile time from annotations
- ✅ It's faster than KAPT because it reads Kotlin natively, no Java stubs
- ✅ A SymbolProcessorProvider creates a SymbolProcessor
- ✅ Room, Hilt, and Moshi all use annotation processing
- ✅ Register processors with the ksp(...) configuration
- ✅ You've completed the advanced Kotlin track!
Practice quiz
What does KSP stand for?
- Kotlin Symbol Processing
- Kotlin Standard Plugin
- Kotlin Source Packager
- Kotlin Static Profiler
Answer: Kotlin Symbol Processing. KSP is Kotlin Symbol Processing, an API for building lightweight compiler plugins that read code and generate new sources.
When does KSP run relative to your program?
- Only when the app is installed
- During code review
- At compile time, generating code before/while the code compiles
- At runtime, on every launch
Answer: At compile time, generating code before/while the code compiles. KSP is a compile-time tool: it inspects symbols and generates additional source files that are then compiled with your code.
What is the main advantage of KSP over KAPT?
- It runs your app faster at runtime
- It is faster because it understands Kotlin natively instead of generating Java stubs
- It needs no annotations at all
- It replaces Gradle
Answer: It is faster because it understands Kotlin natively instead of generating Java stubs. KAPT generates Java stubs for the Java annotation processing API, which is slow; KSP reads Kotlin directly, making builds significantly faster.
Which Gradle configuration adds a KSP processor to a module?
- annotationProcessor(...)
- implementation(...)
- kapt(...)
- ksp(...)
Answer: ksp(...). You declare a symbol processor with the ksp(...) configuration in your dependencies block, after applying the KSP Gradle plugin.
Which interface do you implement to define a KSP processor?
- SymbolProcessor
- KspPlugin
- CodeGenerator
- AnnotationProcessor
Answer: SymbolProcessor. A processor implements SymbolProcessor; its process() method inspects resolved symbols and emits generated code.
What is the role of SymbolProcessorProvider?
- It signs the APK
- It is a factory that creates your SymbolProcessor instance
- It runs the app
- It downloads dependencies
Answer: It is a factory that creates your SymbolProcessor instance. SymbolProcessorProvider is the entry point KSP discovers; its create() method returns your SymbolProcessor.
Why does KSP generate code at build time rather than using runtime reflection?
- It does not; KSP uses reflection
- To make the APK larger
- Because reflection is unavailable in Kotlin
- For better performance and compile-time safety, avoiding slow runtime reflection
Answer: For better performance and compile-time safety, avoiding slow runtime reflection. Generating code at compile time avoids the runtime cost and unpredictability of reflection and surfaces errors during the build.
Which of these libraries commonly uses KSP for code generation?
- The Android emulator
- OkHttp only
- Room, Hilt, and Moshi
- The Kotlin standard library
Answer: Room, Hilt, and Moshi. Room (DAOs), Hilt/Dagger (the dependency graph), and Moshi (JSON adapters) all generate code via annotation processing, with KSP support.
Where does KSP place the sources it generates?
- In the system temp folder only
- In a generated build directory that is compiled alongside your code
- Inside your src/main folder
- In the Android manifest
Answer: In a generated build directory that is compiled alongside your code. Generated files land in a build/generated output directory and are added to the compilation, so you can reference them like normal code.
Compared with writing the boilerplate by hand, what does annotation processing give you?
- Generated, consistent boilerplate from annotations, reducing manual errors
- A different programming language
- Automatic UI design
- Slower compilation with no benefit
Answer: Generated, consistent boilerplate from annotations, reducing manual errors. Annotation processors generate repetitive code from your annotations, keeping it consistent and freeing you from writing it by hand.
Continue this course
- Previous: Coroutine Exceptions & Cancellation
- Quick reference: Kotlin cheat sheet
Frequently asked questions
What is KSP and what problem does it solve?
KSP — Kotlin Symbol Processing — is an API for writing lightweight compiler plugins that read your code's symbols (classes, functions, properties, annotations) and generate new source files at compile time. The problem it solves is boilerplate: instead of hand-writing repetitive code like Room DAO implementations, dependency-injection wiring, or JSON adapters, you annotate your types and a processor generates that code for you. Because it happens at compile time, the generated code is type-checked and there's no runtime reflection cost.
How is KSP different from KAPT?
KAPT (Kotlin Annotation Processing Tool) lets Kotlin use the older Java annotation processing API. To do that it generates Java stubs of your Kotlin code so Java-based processors can read them, which is slow and a frequent build bottleneck. KSP was built specifically for Kotlin: it understands Kotlin's type system natively — null-safety, extension functions, and so on — without producing Java stubs. The result is significantly faster builds and a cleaner API. Most major libraries now ship KSP processors, and the ecosystem is steadily moving from KAPT to KSP.
How do I write and register a KSP processor?
A processor has two parts. You implement SymbolProcessorProvider, a factory whose create() method returns your processor and receives a SymbolProcessorEnvironment (giving you a CodeGenerator to write files and a KSPLogger to report messages). You implement SymbolProcessor, whose process(resolver) method uses the Resolver to find symbols annotated with your annotation and then emits generated sources. To use any processor, you apply the KSP Gradle plugin and add the processor with the ksp(...) dependency configuration — the analogue of kapt(...) or annotationProcessor(...).
Which libraries use KSP, and where does the generated code go?
Many of the libraries you already use rely on annotation processing and offer KSP support: Room generates the implementations of your @Dao interfaces; Hilt and Dagger generate the dependency-injection graph; Moshi generates reflection-free JSON adapters from @JsonClass types. When KSP runs during the build, it writes the generated .kt files into a build output directory (under build/generated) and adds them to the compilation, so you can reference the generated classes from your own code as if you'd written them yourself. You rarely look at these files directly — you just enjoy the boilerplate being written for you.