skip to content

KSP (Kotlin Symbol Processing)

KSP reads Kotlin declarations directly through a symbol API and emits sources without any stub round-trip. Writing a processor means implementing SymbolProcessor, querying the resolver for annotated symbols, and generating files.

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

questions

5

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

level: juniorimportance: must knowfreq 55%

answer

  1. Provider = factory, Processor = worker
  2. create(environment) -> process(resolver)
  3. META-INF/services registers the Provider
  4. No Java stubs; Kotlin-native symbol model
  5. ksp(...) Gradle configuration, not implementation

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.

solid answer

~30 s

KSP (Kotlin Symbol Processing) is a Kotlin-native API for compile-time metaprogramming: it lets you inspect annotated declarations and generate new Kotlin/Java source files. Two types are mandatory. SymbolProcessorProvider is the factory registered via a META-INF/services file (or the KSP Gradle plugin); KSP instantiates it once and calls create(environment), passing a SymbolProcessorEnvironment that exposes the CodeGenerator, KSPLogger, and options. It returns a SymbolProcessor, whose process(resolver: Resolver): List<KSAnnotated> method is invoked each round. The Resolver is the query API over the program's symbols. Unlike kapt, KSP reads Kotlin directly through its own symbol model, so it never generates Java stubs.

code

kotlin · 14 lines
kotlin
class BuilderProcessorProvider : SymbolProcessorProvider {
    override fun create(env: SymbolProcessorEnvironment): SymbolProcessor =
        BuilderProcessor(env.codeGenerator, env.logger)
}

class BuilderProcessor(
    private val codeGenerator: CodeGenerator,
    private val logger: KSPLogger,
) : SymbolProcessor {
    override fun process(resolver: Resolver): List<KSAnnotated> {
        logger.info("KSP round running")
        return emptyList()
    }
}

go deeper

for a junior

Can state KSP is compile-time, names SymbolProcessor and SymbolProcessorProvider, and knows it generates source files.

for a middle

Explains the Provider/Processor split, the create->process flow, and how registration via META-INF/services and the ksp(...) configuration works.

for a senior

Contrasts KSP's Kotlin-native symbol model with stub generation, discusses the SymbolProcessorEnvironment contents, and rounds/deferral semantics.

for a principal

Reasons about processor design for incrementality and multimodule builds, version-compatibility of the KSP API surface, and library packaging of processors.

## What KSP is **KSP (Kotlin Symbol Processing)** is a Kotlin compiler plugin and API for **compile-time metaprogramming**. A *symbol processor* reads the *symbols* (declarations like classes, functions, properties) in the program being compiled, and can **generate new source files**. Typical uses: generating boilerplate, builders, serializers, or DI wiring from annotations. KSP models Kotlin's own language constructs (it understands `suspend`, nullability, extension functions, type aliases) instead of going through Java's `javax.lang.model`. Crucially, it **does not produce Java stubs**, which is what makes it faster than the older kapt approach. ## The two entry-point types ```kotlin // 1) The factory — registered & discovered by KSP class MyProcessorProvider : SymbolProcessorProvider { override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor = MyProcessor(environment.codeGenerator, environment.logger, environment.options) } // 2) The worker — does the inspection and code generation class MyProcessor( private val codeGenerator: CodeGenerator, private val logger: KSPLogger, private val options: Map<String, String>, ) : SymbolProcessor { override fun process(resolver: Resolver): List<KSAnnotated> { // query the program, emit files, return deferred symbols return emptyList() } } ``` - **`SymbolProcessorProvider`** — the registered factory. KSP discovers it via `src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider` (one fully-qualified class name per line). Its `create(environment)` receives a **`SymbolProcessorEnvironment`** exposing `codeGenerator`, `logger` (`KSPLogger`), `options`, and the KSP/Kotlin version. It returns the processor. - **`SymbolProcessor`** — the worker. Its single required method `process(resolver: Resolver): List<KSAnnotated>` runs the actual logic. The returned list is the set of symbols that **could not be processed yet** (deferred to a later round). ## How it is wired in Gradle Apply the `com.google.devtools.ksp` plugin and declare the processor with the `ksp(...)` configuration (not `implementation`): ```kotlin plugins { id("com.google.devtools.ksp") version "<ver>" } dependencies { ksp("com.example:my-processor:1.0") } ``` ## Key terms - **Symbol** — a declaration the compiler knows about (class, function, property, parameter). - **Round** — one pass of `process()`; KSP runs multiple rounds so newly generated code can itself be processed. - **Generated sources** — files written through the `CodeGenerator`, automatically added to the compilation.

  • Where do you register a SymbolProcessorProvider so KSP finds it?
    In src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider, listing the provider's fully-qualified class name.
  • Which Gradle configuration declares a KSP processor dependency?
    The ksp(...) configuration added by the KSP Gradle plugin, e.g. ksp("group:artifact:version"); using implementation would put it on the runtime classpath instead of running it as a processor.

The Provider is the staffing agency that hires one worker (the Processor); each round the worker is handed a Resolver — a searchable index of the codebase — and writes new files.

saying these in an interview costs you the question

  • Thinking KSP runs at runtime via reflection instead of at compile time
  • Claiming you implement only one type — forgetting the Provider factory
  • Saying KSP generates Java stubs like kapt does
  • Registering the Processor (not the Provider) in META-INF/services
  • Declaring the processor with implementation instead of ksp(...)

context

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

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 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