Skip to main content
  1. Posts/

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:

<plugin>
    <groupId>me.kpavlov.ksp.maven</groupId>
    <artifactId>ksp-maven-plugin</artifactId>
    <version>0.1.1</version>
    <executions>
        <execution>
            <goals>
                <goal>process</goal>
            </goals>
        </execution>
    </executions>
    <!-- Add your KSP processor as a plugin dependency (not project dependency!) -->
    <dependencies>
        <dependency>
            <groupId>com.example</groupId>
            <artifactId>your-ksp-processor</artifactId>
            <version>1.0.0</version>
        </dependency>
    </dependencies>
</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:

/**
 * Annotation to trigger code generation
 */
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)
annotation class GenerateHello(
    val name: String = "World",
)

/**
 * A simple KSP processor for testing.
 * Generates a greeting class for each class annotated with @GenerateHello.
 */
class HelloProcessor(
    private val codeGenerator: CodeGenerator,
    private val logger: KSPLogger,
) : SymbolProcessor {

    override fun process(resolver: Resolver): List<KSAnnotated> {
        logger.warn("Looking for annotation: ${GenerateHello::class.qualifiedName}")

        val symbols = resolver.getSymbolsWithAnnotation(GenerateHello::class.qualifiedName!!)
        val symbolsList = symbols.toList()
        logger.warn("Found ${symbolsList.size} symbols with @GenerateHello annotation")

        symbolsList
            .filter { it.validate() }
            .filterIsInstance<KSClassDeclaration>()
            .forEach { classDeclaration ->
                logger.warn("Processing class: ${classDeclaration.qualifiedName?.asString()}")
                processClass(classDeclaration)
            }
        
        return emptyList()
    }

    private fun processClass(classDeclaration: KSClassDeclaration) {
        val packageName = classDeclaration.packageName.asString()
        val className = classDeclaration.simpleName.asString()
        val generatedClassName = "${className}Greeting"

        // Get annotation parameter
        val annotation =
            classDeclaration.annotations.first {
                it.shortName.asString() == "GenerateHello"
            }
        val name =
            annotation.arguments
                .firstOrNull { it.name?.asString() == "name" }
                ?.value
                ?.toString() ?: "World"

        logger.info("Generating $generatedClassName for $className with name=$name")

        // Generate the greeting class
        val file =
            codeGenerator.createNewFile(
                dependencies = Dependencies(true, classDeclaration.containingFile!!),
                packageName = packageName,
                fileName = generatedClassName,
            )

        file.bufferedWriter().use { writer ->
            writer.write(
                // language=kotlin
                """
                package $packageName

                /**
                 * Generated greeting class for $className
                 */
                class $generatedClassName {
                    fun greet(): String = "Hello, $name!"

                    companion object {
                        const val GENERATED_FOR = "$className"
                    }
                }
                """.trimIndent(),
            )
        }
    }
}

(source)

When applied to class:

@GenerateHello(name = "Integration Test")
class TestClass {
    fun sayHello() = println("Hello from TestClass")
}

it generates:

/**
 * Generated greeting class for TestClass
 */
class TestClassGreeting {
    fun greet(): String = "Hello, Integration Test!"

    companion object {
        const val GENERATED_FOR = "TestClass"
    }
}

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 #