Skip to main content

Kotlin Symbol Processing

Introduction

Kotlin Symbol Processing (KSP) is a powerful compile-time API designed to process Kotlin code and generate new code during compilation. It serves as an alternative to traditional annotation processing in Java (using the Java Annotation Processing API or KAPT) but is specifically optimized for Kotlin. KSP was created by Google to address the limitations of using KAPT with Kotlin, offering better performance and a more Kotlin-friendly API.

In this tutorial, you'll learn:

  • What KSP is and why it's beneficial
  • How KSP compares to other annotation processing options
  • How to set up and use KSP in your projects
  • Creating your own KSP processors
  • Real-world applications of KSP

Why Use Kotlin Symbol Processing?

Before diving into KSP, let's understand why it's an important tool in a Kotlin developer's toolkit:

  1. Performance: KSP is significantly faster than KAPT (sometimes up to 2x faster build times)
  2. Kotlin-centric: Designed specifically for Kotlin, it understands Kotlin-specific language features
  3. Multiplatform support: While KAPT only works with JVM, KSP can work across different Kotlin platforms
  4. Incremental processing: Better support for incremental compilation

KSP vs KAPT: Understanding the Difference

KAPT (Kotlin Annotation Processing Tool) was the original way to use Java annotation processors with Kotlin code. However, KAPT has several limitations:

  • It converts Kotlin code to Java stubs, which is inefficient
  • It doesn't understand Kotlin-specific features well
  • It's slower than native annotation processing

KSP addresses these issues by working directly with Kotlin's code model, providing a more efficient and Kotlin-friendly API.

Setting Up KSP in Your Project

Let's start by adding KSP to a Kotlin project:

For a Gradle Project:

kotlin
// In your project's build.gradle
plugins {
kotlin("jvm") version "1.6.10" // Your Kotlin version
id("com.google.devtools.ksp") version "1.6.10-1.0.4" // KSP version should match Kotlin version
}

dependencies {
implementation(kotlin("stdlib"))
// If using a library that has KSP processors
ksp("com.example:library-ksp-processor:1.0.0")
}

Creating a KSP Processor:

To create a KSP processor, you need to create a separate module that will contain your processor code:

  1. Create a new module for your processor
  2. Add KSP API dependencies
  3. Implement the SymbolProcessor interface
  4. Register your processor using the service loader mechanism

Let's look at a simplified example:

kotlin
// In your processor module
dependencies {
implementation("com.google.devtools.ksp:symbol-processing-api:1.6.10-1.0.4")
}

class MyProcessor : SymbolProcessor {
lateinit var codeGenerator: CodeGenerator
lateinit var logger: KSPLogger

override fun init(
options: Map<String, String>,
kotlinVersion: KotlinVersion,
codeGenerator: CodeGenerator,
logger: KSPLogger
) {
this.codeGenerator = codeGenerator
this.logger = logger
}

override fun process(resolver: Resolver): List<KSAnnotated> {
// Process code here
// Return any deferred symbols that couldn't be processed
return emptyList()
}

override fun finish() {
// Finalize processing
}

override fun onError() {
// Handle errors
}
}

Then register your processor:

# Create a file at src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessor
com.example.MyProcessor

Creating Your First KSP Processor: A Simple Example

Let's create a basic KSP processor that generates a companion factory method for classes annotated with @Factory.

First, we'll define our annotation:

kotlin
// In your shared module
package com.example

annotation class Factory

Now, let's implement our processor:

kotlin
class FactoryProcessor : SymbolProcessor {
private lateinit var codeGenerator: CodeGenerator
private lateinit var logger: KSPLogger

override fun init(
options: Map<String, String>,
kotlinVersion: KotlinVersion,
codeGenerator: CodeGenerator,
logger: KSPLogger
) {
this.codeGenerator = codeGenerator
this.logger = logger
}

override fun process(resolver: Resolver): List<KSAnnotated> {
// Find all classes with the @Factory annotation
val factoryClasses = resolver
.getSymbolsWithAnnotation("com.example.Factory")
.filterIsInstance<KSClassDeclaration>()
.toList()

if (factoryClasses.isEmpty()) {
return emptyList()
}

factoryClasses.forEach { classDecl ->
generateFactoryExtension(classDecl)
}

return emptyList()
}

private fun generateFactoryExtension(classDecl: KSClassDeclaration) {
val packageName = classDecl.packageName.asString()
val className = classDecl.simpleName.asString()

val file = codeGenerator.createNewFile(
dependencies = Dependencies(false, classDecl.containingFile!!),
packageName = packageName,
fileName = "${className}Factory"
)

file.writer().use { writer ->
writer.write("""
package $packageName

object ${className}Factory {
fun create(): $className {
return $className()
}
}
""".trimIndent())
}
}
}

Usage Example:

With this processor, when you annotate a class like:

kotlin
package com.example

@Factory
class User(val name: String, val age: Int) {
constructor() : this("Unknown", 0)
}

KSP will generate:

kotlin
package com.example

object UserFactory {
fun create(): User {
return User()
}
}

Real-World Applications of KSP

1. JSON Serialization

One common use of KSP is to generate serialization code. Here's a simplified example of how you might create a JSON serialization processor:

kotlin
// Define annotation
@Target(AnnotationTarget.CLASS)
annotation class JsonSerializable

// Processor implementation (simplified)
class JsonSerializationProcessor : SymbolProcessor {
override fun process(resolver: Resolver): List<KSAnnotated> {
val symbols = resolver.getSymbolsWithAnnotation("com.example.JsonSerializable")
.filterIsInstance<KSClassDeclaration>()

symbols.forEach { classDecl ->
// Generate toJson() and fromJson() extension functions
generateJsonMethods(classDecl)
}

return emptyList()
}

private fun generateJsonMethods(classDecl: KSClassDeclaration) {
// Generate serialization code...
}
}

2. Dependency Injection

KSP can be used to implement compile-time dependency injection similar to Dagger:

kotlin
// Define annotations
@Target(AnnotationTarget.CLASS)
annotation class Component

@Target(AnnotationTarget.FUNCTION)
annotation class Provides

// A processor would scan these annotations and generate the appropriate factory code

3. Data Binding

KSP can generate code to bind UI elements to data models:

kotlin
// Define annotation
@Target(AnnotationTarget.FIELD)
annotation class BindView(val id: Int)

// A processor would scan these annotations and generate view binding code

Working with the KSP API

KSP provides a rich API for exploring code:

  1. Resolver: The entry point to the code model
  2. KSNode: The base interface for all Kotlin symbols
  3. KSClassDeclaration: Represents a class or interface
  4. KSPropertyDeclaration: Represents a property
  5. KSFunctionDeclaration: Represents a function

Example: Exploring Class Properties

kotlin
fun processClass(classDecl: KSClassDeclaration) {
// Get all properties in the class
val properties = classDecl.getAllProperties()

properties.forEach { property ->
val name = property.simpleName.asString()
val type = property.type.resolve().declaration.qualifiedName?.asString()
logger.info("Property: $name of type $type")

// Check for annotations
val annotations = property.annotations.map {
it.annotationType.resolve().declaration.qualifiedName?.asString()
}
logger.info("Annotations: $annotations")
}
}

Best Practices for KSP Processors

  1. Minimize symbol resolution: Resolution is expensive, so cache results when possible
  2. Use incremental processing: Support it by properly specifying dependencies
  3. Generate clean code: Include helpful comments in generated code
  4. Handle errors gracefully: Provide meaningful error messages
  5. Test thoroughly: Use the KSP testing utilities to ensure your processor works correctly

Common Pitfalls and Solutions

1. Missing Type Information

Problem: Type resolution fails with unexpected nulls

Solution: Make sure you check if types are resolved before accessing them:

kotlin
val type = property.type.resolve()
if (type.isError) {
logger.error("Could not resolve type for ${property.simpleName.asString()}")
return
}

2. Incremental Processing Issues

Problem: Changes don't trigger reprocessing

Solution: Properly specify dependencies in createNewFile:

kotlin
codeGenerator.createNewFile(
dependencies = Dependencies(aggregating = true, *sourcesFiles.toTypedArray()),
packageName = packageName,
fileName = fileName
)

3. Registration Issues

Problem: Processor isn't being picked up

Solution: Verify your service file is correctly placed at: src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessor

Summary

Kotlin Symbol Processing (KSP) is a powerful tool for code generation that offers significant advantages over traditional annotation processing:

  • It's faster and more efficient than KAPT
  • It works natively with Kotlin syntax and features
  • It supports multiplatform projects
  • It provides a rich API for exploring and manipulating code

Through this tutorial, you've learned:

  • How to set up KSP in your projects
  • Creating basic KSP processors
  • Exploring the code model with KSP API
  • Real-world applications of KSP
  • Best practices and common pitfalls

As you become more comfortable with KSP, you'll find it's an invaluable tool for creating compile-time code generation solutions that improve both developer experience and application performance.

Additional Resources

Exercises

  1. Create a KSP processor that generates toString() methods for classes marked with a custom annotation.
  2. Extend your processor to handle data validation by generating validation methods based on field annotations.
  3. Build a simple dependency injection framework using KSP.
  4. Create a KSP processor that generates builder patterns for annotated classes.
  5. Implement a processor that generates event handling code for annotated methods.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)