Weekend hack: Kotlin Symbol Processing Maven plugin

The Problem

KSP (Kotlin Symbol Processing) is what powers libraries like Room, Moshi, and Dagger to generate boilerplate code from your annotations. It’s faster than kapt and built for Kotlin from the ground up.

The catch? Official support only exists for Gradle. If you’re using Maven, you’re out of luck.

The Solution

I built ksp-maven-plugin to fix this. It’s now on Maven Central.

It runs during generate-sources, auto-discovers KSP processors from your dependencies, and generates code before compilation. No manual configuration - just add the plugin to your pom.xml and you’re done.

Getting Started

You’ll need Maven 3.6.0+, JDK 11+, and Kotlin 2.2+.

Add this to your pom.xml:

xml
 1<plugin>
 2    <groupId>me.kpavlov.ksp.maven</groupId>
 3    <artifactId>ksp-maven-plugin</artifactId>
 4    <version>0.1.1</version>
 5    <executions>
 6        <execution>
 7            <goals>
 8                <goal>process</goal>
 9            </goals>
10        </execution>
11    </executions>
12    <!-- Add your KSP processor as a plugin dependency (not project dependency!) -->
13    <dependencies>
14        <dependency>
15            <groupId>com.example</groupId>
16            <artifactId>your-ksp-processor</artifactId>
17            <version>1.0.0</version>
18        </dependency>
19    </dependencies>
20</plugin>

Understanding KSP

KSP reads your code (classes, functions, properties) and generates new source files based on annotations or other conditions. Then the Kotlin compiler compiles everything together. No bytecode manipulation like Lombok.

One important thing: processors can only read your code and generate new files. They can’t modify existing sources.

Here’s a simple example - the HelloProcessor generates a greeting class for any class annotated with @GenerateHello:

kotlin
 1/**
 2 * Annotation to trigger code generation
 3 */
 4@Target(AnnotationTarget.CLASS)
 5@Retention(AnnotationRetention.SOURCE)
 6annotation class GenerateHello(
 7    val name: String = "World",
 8)
 9
10/**
11 * A simple KSP processor for testing.
12 * Generates a greeting class for each class annotated with @GenerateHello.
13 */
14class HelloProcessor(
15    private val codeGenerator: CodeGenerator,
16    private val logger: KSPLogger,
17) : SymbolProcessor {
18
19    override fun process(resolver: Resolver): List<KSAnnotated> {
20        logger.warn("Looking for annotation: ${GenerateHello::class.qualifiedName}")
21
22        val symbols = resolver.getSymbolsWithAnnotation(GenerateHello::class.qualifiedName!!)
23        val symbolsList = symbols.toList()
24        logger.warn("Found ${symbolsList.size} symbols with @GenerateHello annotation")
25
26        symbolsList
27            .filter { it.validate() }
28            .filterIsInstance<KSClassDeclaration>()
29            .forEach { classDeclaration ->
30                logger.warn("Processing class: ${classDeclaration.qualifiedName?.asString()}")
31                processClass(classDeclaration)
32            }
33        
34        return emptyList()
35    }
36
37    private fun processClass(classDeclaration: KSClassDeclaration) {
38        val packageName = classDeclaration.packageName.asString()
39        val className = classDeclaration.simpleName.asString()
40        val generatedClassName = "${className}Greeting"
41
42        // Get annotation parameter
43        val annotation =
44            classDeclaration.annotations.first {
45                it.shortName.asString() == "GenerateHello"
46            }
47        val name =
48            annotation.arguments
49                .firstOrNull { it.name?.asString() == "name" }
50                ?.value
51                ?.toString() ?: "World"
52
53        logger.info("Generating $generatedClassName for $className with name=$name")
54
55        // Generate the greeting class
56        val file =
57            codeGenerator.createNewFile(
58                dependencies = Dependencies(true, classDeclaration.containingFile!!),
59                packageName = packageName,
60                fileName = generatedClassName,
61            )
62
63        file.bufferedWriter().use { writer ->
64            writer.write(
65                // language=kotlin
66                """
67                package $packageName
68
69                /**
70                 * Generated greeting class for $className
71                 */
72                class $generatedClassName {
73                    fun greet(): String = "Hello, $name!"
74
75                    companion object {
76                        const val GENERATED_FOR = "$className"
77                    }
78                }
79                """.trimIndent(),
80            )
81        }
82    }
83}

(source)

When applied to class:

kotlin
1@GenerateHello(name = "Integration Test")
2class TestClass {
3    fun sayHello() = println("Hello from TestClass")
4}

it generates:

kotlin
 1/**
 2 * Generated greeting class for TestClass
 3 */
 4class TestClassGreeting {
 5    fun greet(): String = "Hello, Integration Test!"
 6
 7    companion object {
 8        const val GENERATED_FOR = "TestClass"
 9    }
10}

Wrapping Up

That’s it. KSP was Gradle-only, now it works with Maven too. The plugin is on Maven Central and ready to use.

Give it a shot in your next project. Got questions or issues? Open an issue on GitHub.

Resources

Konstantin Pavlov

Konstantin Pavlov

Software Engineer working with Java, Kotlin, Swift, and AI. Focusing on software architecture and building AI-infused apps. Passionate about testing and Open-Source projects.