KSP emits sources "without Java stubs." What does that mean concretely, and what are the practical consequences for a processor reading Kotlin code?
answer
- kapt = Kotlin -> Java stubs -> apt; slow + lossy
- KSP reads Kotlin directly, no stub phase
- Sees nullability, suspend, extensions, value class, typealias
- Resolver replaces Elements/RoundEnvironment
- Generates new files only; can't rewrite existing code
basics
~20 sOlder 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.
solid answer
~40 sLegacy Kotlin annotation processing (kapt) ran by generating Java *stub* source — synthetic .java declarations mirroring the Kotlin API — so a standard javax.annotation.processing.Processor working over javax.lang.model could read them. Stub generation is expensive and lossy. KSP instead exposes a **Kotlin-native symbol model** (KSClassDeclaration, KSFunctionDeclaration, KSType, etc.) backed by the compiler's own analysis, so no stubs are produced. Consequences: builds are faster (no stub phase); processors see Kotlin-specific concepts faithfully — nullability (KSType.isMarkedNullable), suspend (Modifier.SUSPEND), extension/receiver types, default arguments, top-level functions, value classes, type aliases — that Java's model can't represent; and you query through Resolver rather than RoundEnvironment/Elements. The tradeoff is that you write against KSP's API, not the standard JSR-269 one.
go deeper
Understands that 'no stubs' means KSP reads Kotlin directly and is faster than the old approach.
Names the stub phase, that KSP uses a Kotlin-native model and Resolver, and that it only generates new files.
Explains fidelity gains (nullability, suspend, extensions, value classes), the JSR-269 contrast, and the new-files-only limitation.
Weighs ecosystem implications: multiplatform support, migration cost from JSR-269 processors, and why true code transformation needs a compiler plugin instead.
## What a "Java stub" is Kotlin's original annotation-processing bridge, **kapt**, reused the Java annotation-processing infrastructure (**JSR-269**: `javax.annotation.processing.Processor`, `javax.lang.model.element.Element`, `RoundEnvironment`). But those APIs only understand **Java** declarations. So kapt first compiled Kotlin to a set of synthetic **Java source stubs** — `.java` files containing the *signatures* (not bodies) of your Kotlin declarations — purely so the Java apt machinery could inspect them. Generating stubs has costs: - It's a **separate, slow compilation phase** run before processing. - It's **lossy**: Kotlin concepts with no Java equivalent (nullable types, `suspend`, extension receivers, default values, top-level functions, `value`/inline classes, type aliases, named companions) are either erased or awkwardly approximated. ## What "without Java stubs" means in KSP KSP plugs directly into the Kotlin compiler and exposes a **Kotlin-native symbol model**. There is **no stub phase at all**. Instead of `Element`/`TypeMirror` you work with: - **`KSClassDeclaration`**, **`KSFunctionDeclaration`**, **`KSPropertyDeclaration`**, **`KSValueParameter`** — Kotlin declarations. - **`KSType`** / **`KSTypeReference`** — Kotlin types, with `isMarkedNullable`, type arguments, `starProjection()`, etc. - **`Resolver`** — the query entry point (replacing `Elements`/`RoundEnvironment`). ```kotlin // Kotlin-specific facts KSP exposes directly (kapt's Java model can't): val isSuspend = func.modifiers.contains(Modifier.SUSPEND) val isNullable = param.type.resolve().isMarkedNullable val receiver = func.extensionReceiver // extension functions val hasDefault = param.hasDefault // default arguments ``` ## Practical consequences 1. **Speed** — skipping stub generation removes an entire compile pass; KSP is typically much faster, especially incrementally. 2. **Fidelity** — processors can reason about real Kotlin: nullability, `suspend`, variance/`in`/`out`, `value class`, `typealias`, top-level/extension functions, properties with custom getters. A stub-based processor would never see some of these. 3. **Different API** — you don't get `RoundEnvironment.getElementsAnnotatedWith(...)`; you use `resolver.getSymbolsWithAnnotation(...)`. Rounds and deferral are modeled by returning unresolved `KSAnnotated` from `process()`. 4. **Output is still source** — KSP doesn't modify existing bytecode/AST; like apt, it only **generates new files** via the `CodeGenerator`. You cannot rewrite an existing function body. 5. **Multiplatform** — because KSP is Kotlin-native (not tied to the JVM/Java stubs), it can run on Kotlin/Native and Kotlin/JS targets, which kapt (JVM/Java-only) cannot. ## What KSP is NOT - It is **not** a general macro system — it cannot transform or replace existing declarations, only add new files. - It does **not** give you bytecode access; it's a *symbol* (signature-level) view plus code generation. ## Key terms - **Stub** — synthetic Java source mirroring Kotlin signatures, used by kapt. - **JSR-269 / apt** — the Java annotation-processing API kapt reused. - **Symbol model** — KSP's Kotlin-native representation (KSClassDeclaration, KSType, ...). - **isMarkedNullable / Modifier.SUSPEND** — Kotlin facts visible to KSP but not to Java stubs.
- Name two Kotlin features a stub-based processor loses that KSP sees directly.Nullability (KSType.isMarkedNullable) and suspend modifiers (Modifier.SUSPEND). Others: extension receivers, default arguments, top-level functions, value/inline classes, and type aliases — none of which map cleanly to Java's element model.
- Can a KSP processor modify the body of an existing function?No. Like apt, KSP only generates new source files via CodeGenerator; it cannot rewrite or replace existing declarations or bytecode. Transforming existing code requires a full compiler plugin, not KSP.
kapt is like translating a Kotlin novel into clumsy English (Java stubs) before anyone can read it; KSP hires a native Kotlin reader, so nothing is lost and there's no translation step.
saying these in an interview costs you the question
- Claiming KSP can rewrite/transform existing code like a macro
- Saying KSP still generates Java stubs internally
- Confusing KSP's Resolver with Java's RoundEnvironment/Elements API
- Asserting kapt is faster than KSP
- Believing KSP gives bytecode-level access