skip to content

Annotation Processing

Generating code from annotations at compile time, historically through kapt's Java-stub bridge and now through KSP's native Kotlin symbol API. Build speed is the reason this topic keeps coming up.

part ofKotlinoverview, primer and where to startread it →
on this pageshow

explore

questions

20

You're adding Room to a Kotlin module. Which Gradle dependency configuration do you use to wire up Room's annotation processor, and what else must you add to the build?

level: juniorimportance: must knowfreq 70%

answer

  1. ksp(room-compiler), not implementation
  2. Apply the KSP Gradle plugin first
  3. room-runtime + room-ktx + ksp compiler
  4. Room generates *_Impl classes
  5. Room is KSP-native — prefer ksp over kapt

basics

~20 s

You add the Room library as a normal dependency, and put Room's compiler on the ksp configuration (the modern way) instead of a plain dependency. You also apply the KSP Gradle plugin so ksp(...) exists.

solid answer

~30 s

Apply the KSP plugin (com.google.devtools.ksp), then in dependencies use implementation("androidx.room:room-runtime") and the Kotlin extensions implementation("androidx.room:room-ktx"), and crucially ksp("androidx.room:room-compiler") so the compiler that generates the *_Impl DAO/database classes runs. Room ships native KSP support, so prefer ksp(...) over kapt(...). The ksp() configuration tells KSP which artifact contains the SymbolProcessorProvider; without the plugin applied, ksp(...) is unresolved. With kapt you'd instead apply org.jetbrains.kotlin.kapt and use kapt("androidx.room:room-compiler"), but that path is slower and discouraged for Room.

code

kotlin · 10 lines
kotlin
plugins {
    id("org.jetbrains.kotlin.jvm")
    id("com.google.devtools.ksp")
}

dependencies {
    implementation("androidx.room:room-runtime")
    implementation("androidx.room:room-ktx")
    ksp("androidx.room:room-compiler")
}

go deeper

for a junior

Knows the compiler goes on ksp(...) and that the KSP plugin must be applied.

for a middle

Explains why implementation() doesn't invoke the processor and that Room is KSP-native.

for a senior

Discusses generated-source wiring, room-ktx vs runtime split, and the kapt fallback tradeoff.

for a principal

Can reason about migrating a whole multi-module build off kapt and standardizing the KSP version via the version catalog/convention plugins.

## What Room needs at build time Room generates code (DAO implementations like `UserDao_Impl`, the database `_Impl`) from your `@Entity`, `@Dao`, and `@Database` annotations. That code is produced by an **annotation processor**, which must run during compilation. In Gradle you express "run this processor" by placing the processor artifact on a special dependency **configuration**. ## The two configurations - **`ksp(...)`** — Kotlin Symbol Processing. The modern, Kotlin-native processor API. Faster because it reads Kotlin source directly (no Java stubs). - **`kapt(...)`** — Kotlin Annotation Processing Tool. The legacy bridge that generates Java stubs so Java-based (`javax.annotation.processing`) processors run. Slower. Room **ships native KSP support**, so use `ksp(...)`. ## Required pieces 1. **Apply the KSP plugin** so the `ksp(...)` configuration exists: ```kotlin plugins { id("com.google.devtools.ksp") } ``` 2. **Add dependencies** — runtime as a normal dependency, compiler on `ksp`: ```kotlin dependencies { implementation("androidx.room:room-runtime") implementation("androidx.room:room-ktx") // coroutines/Flow support ksp("androidx.room:room-compiler") // the processor } ``` ## Why not just `implementation` for the compiler? If you put `room-compiler` on `implementation`, the processor jar lands on the runtime classpath but is **never invoked**, so no `_Impl` classes are generated and you get "cannot find implementation" errors. The configuration name is what activates the processor. ## kapt alternative (discouraged for Room) ```kotlin plugins { id("org.jetbrains.kotlin.kapt") } dependencies { kapt("androidx.room:room-compiler") } ``` This works but is slower. Prefer KSP.

  • What happens if you apply the KSP plugin but forget the ksp("androidx.room:room-compiler") line?
    The configuration exists but no Room processor is registered, so no DAO/database implementation classes are generated and the build fails with missing _Impl classes.
  • Where does the generated code end up?
    Under build/generated/ksp/<variant>/kotlin (or .../java), and KSP automatically adds it to the source set so it compiles.

ksp(...) is like a backstage pass: it lets the processor onstage to generate code; a plain dependency just leaves it in the audience.

saying these in an interview costs you the question

  • Putting room-compiler on implementation or api
  • Forgetting to apply the KSP (or kapt) plugin
  • Thinking ksp() and implementation() are interchangeable
  • Claiming Room requires kapt

context

open as a page

What is kapt and what problem does the kotlin-kapt Gradle plugin solve for Kotlin projects?

level: juniorimportance: must knowfreq 60%

basics

~20 s

kapt is a Kotlin plugin that lets old Java annotation-processing tools (like Dagger or Room) work on Kotlin code. It does this by first turning Kotlin into simple Java placeholder files those tools can read.

open as a page

Why is KSP generally faster than kapt for annotation processing in a Kotlin project?

level: juniorimportance: must knowfreq 70%

basics

~10 s

kapt first turns your Kotlin into fake Java code so old Java tools can read it, which is slow. KSP reads your Kotlin code directly, skipping that extra step, so the build is faster.

open as a page

What is KSP (Kotlin Symbol Processing), and what are the two entry-point types a processor author must implement?

level: juniorimportance: must knowfreq 55%

basics

~10 s

KSP is Kotlin's tool for reading code at compile time and generating new source files. You write a SymbolProcessor that does the work, and a SymbolProcessorProvider that creates it.

open as a page

Walk through configuring kapt for a Java annotation processor in a Gradle Kotlin DSL build, including how processor arguments are passed.

level: middleimportance: must knowfreq 50%

basics

~10 s

Apply the kotlin-kapt plugin, then add the processor with the kapt(...) line instead of implementation(...). If the processor needs options, pass them in a kapt { arguments { arg(...) } } block.

open as a page

How do you find all declarations annotated with a given annotation in a KSP processor, and how do you narrow the results to classes vs. functions?

level: middleimportance: must knowfreq 50%

basics

~10 s

Call resolver.getSymbolsWithAnnotation("com.example.MyAnnotation") to get a sequence of annotated symbols, then filter to the kind you want, for example keeping only KSClassDeclaration or KSFunctionDeclaration.

open as a page

How does the CodeGenerator emit a new Kotlin source file, and what is the purpose of the Dependencies argument it requires?

level: seniorimportance: must knowfreq 42%

basics

~10 s

Call codeGenerator.createNewFile(...) to get an output stream and write Kotlin code into it. The Dependencies argument tells KSP which input files the generated file came from, so incremental builds know when to regenerate it.

open as a page

A teammate wired Dagger/Hilt with kapt and asks whether they can just switch to ksp(...) for faster builds. How do you advise them, and how does the Gradle wiring differ for Hilt specifically?

level: middleimportance: should knowfreq 55%

basics

~10 s

Recent Dagger/Hilt versions support KSP, so they can move the compilers from kapt(...) to ksp(...). Hilt also needs its own Gradle plugin applied, regardless of kapt or KSP.

open as a page

How do you configure Moshi so that its adapters are generated at compile time, and how does that wiring differ from Moshi's reflection-based approach?

level: middleimportance: should knowfreq 45%

basics

~10 s

For compile-time adapters you add Moshi plus its codegen artifact on the ksp(...) configuration and mark classes @JsonClass(generateAdapter = true). The reflection approach instead adds moshi-kotlin and needs no processor at all.

open as a page

Why is kapt's stub-generation step a performance concern, and what does it cost at the build level?

level: middleimportance: should knowfreq 55%

basics

~10 s

Before processing, kapt makes kotlinc produce extra Java placeholder files for your whole codebase. Generating those files runs an additional, expensive compilation pass, which makes builds noticeably slower.

open as a page

You're migrating an Android module from kapt to KSP. Walk through the migration path and what determines whether you can migrate at all.

level: middleimportance: should knowfreq 50%

basics

~20 s

First check that each library you use offers a KSP version of its processor. If it does, swap the Gradle plugin and change kapt(...) to ksp(...) for that library. If a library only has a kapt processor, you must keep kapt for it.

open as a page

Why can KSP work in Kotlin Multiplatform projects while kapt cannot, and what does that imply for cross-platform code generation?

level: middleimportance: should knowfreq 45%

basics

~10 s

kapt needs a Java compiler, which only exists on the JVM, so it can't run for iOS or JavaScript targets. KSP is a pure Kotlin compiler plugin, so it runs on every Kotlin platform.

open as a page

Across Room, Dagger/Hilt, and Moshi, which processors ship native KSP support and which were historically kapt-only? How does that influence how you set up a module's build today?

level: seniorimportance: should knowfreq 50%

basics

~20 s

Room and Moshi codegen have supported KSP for a while; Dagger/Hilt were kapt-only for years but modern versions added KSP. Today you wire all three via ksp(...) and only fall back to kapt for processors that still lack KSP.

open as a page

What does the kapt correctErrorTypes option do, and when do you need to enable it?

level: seniorimportance: should knowfreq 40%

basics

~20 s

correctErrorTypes tells kapt to recover the real types of code that is itself still being generated. Without it, those types show up as a meaningless 'error' placeholder and processors break. It is off by default.

open as a page

KSP emits sources "without Java stubs." What does that mean concretely, and what are the practical consequences for a processor reading Kotlin code?

level: seniorimportance: should knowfreq 35%

basics

~20 s

Older Kotlin annotation processing first turned Kotlin into fake Java classes (stubs) so Java tools could read them. KSP skips that and reads Kotlin directly through its own model, so it's faster and sees real Kotlin features.

open as a page

Explain multi-round processing in KSP: why does process() run multiple rounds, what does returning deferred symbols mean, and how does this compare to kapt's rounds?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Some generated code creates new things other processors must then look at. So processing runs in repeated rounds until nothing new appears. In KSP, a processor returns the symbols it couldn't fully handle yet, and they're retried in the next round.

open as a page

Explain KSP's multi-round model: why does process() return a List<KSAnnotated>, and how does deferral interact with code that is generated during processing?

level: principalimportance: should knowfreq 28%

basics

~20 s

KSP runs process() several times (rounds). If a symbol can't be handled yet because the types it needs don't exist yet, you return it so KSP tries again next round, after more code has been generated.

open as a page

Despite KSP's advantages, when would you still be forced to use kapt, and how do you reason about that trade-off on a real project?

level: seniorimportance: nice to knowfreq 25%

basics

~10 s

You must keep kapt when a library only provides an old Java-style annotation processor and no KSP version. kapt works with those because it runs the standard Java processing pipeline; KSP can't run them.

open as a page

In a multi-module Kotlin build using Room and Hilt via KSP, how do you pass processor options (e.g., Room's schema export directory) and keep the KSP/processor versions consistent across modules?

level: principalimportance: nice to knowfreq 25%

basics

~20 s

You pass processor options through the ksp { arg(...) } block in the module (for example Room's schema directory), and you keep versions aligned by centralizing them in a version catalog or a shared convention plugin so every module uses the same KSP and processor versions.

open as a page

kapt is described as legacy. As a principal engineer, how would you decide whether and how to migrate a large multi-module project off kapt?

level: principalimportance: nice to knowfreq 30%

basics

~10 s

Check which of your annotation processors have a faster KSP version. Migrate those module-by-module, keep kapt only for the few processors that have no KSP option, and measure build-time improvement before and after.

open as a page