Make Kotlin and Groovy work together

Thomas Martin
WhozApp
Published in
9 min readSep 29, 2023

How to make those two work together, as smoothly as possible.

Photo by Ave Calvar on Unsplash

The development of Whoz started in 2016. At that time, the team chose Groovy as our back-end programming language. Technology moves fast, and 3 years later, the team decided that we’d go on with Kotlin. Brand-new code would have to be in Kotlin, but the legacy Groovy codebase would still be maintained.

Unlike Groovy and Java, or Kotlin and Java, mixing Kotlin and Groovy is not seamless. Let me explain the caveats and tricks we learned to smoothen the integration.

Two steps compilation

Configure with Gradle

Groovy and Kotlin cannot be compiled together. You need to compile one, then the other.

With Gradle, you’ll get something like this:

compileGroovy.dependsOn compileKotlin
compileGroovy.classpath += files(compileKotlin.destinationDir)
classes.dependsOn compileGroovy

This also means that they have their own directory:

  • src/main/groovy and src/test/groovyfor Groovy
  • src/main/kotlin and src/test/kotlinfor Kotlin

In the piece of Gradle configuration above, the Groovy compilation depends on the Kotlin one. That means the Kotlin is compiled first, then Groovy.

Consequently, your Kotlin code cannot call your Groovy code, but the Groovy code can call your Kotlin code.

You can make the opposite choice, by compiling Groovy first, your Kotlin code can call the Groovy code, but the other way around is no longer possible.

Groovy first or Kotlin first?

While I’ll give you tricks to call indirectly Groovy code from Kotlin if you choose Kotlin-first compilation, this choice is essential and almost definitive, so you need to think about it carefully.

Let’s take a classic Controller-Service-Domain app written in Groovy as an example. That was the situation we had at Whoz in 2019.

The most isolated part is the Domain one, it does not need to call the service or the controller. So you can migrate this part in Kotlin, even if it is compiled first. The same is true for POJOs representing inputs and outputs of the controller’s endpoints.

With Groovy-first compilation, the controller is the more accessible part to migrate. Since you can call Groovy from Kotlin, there is no issue with replacing the controller. Once done, you can migrate the service and input and output POJOs. Finally, the domain can be migrated.

Of course, you can write the Controller-Service-Domain entirely in Kotlin for totally new entities, whatever compilation sequence is chosen.

At Whoz, we chose to compile Kotlin first. Having the experience we have now, I don’t think we made the best choice. With Groovy-first compilation, the path to migrate is clear (controller, then service, then domain). Moreover, Kotlin-first compile was chosen to replace domain and payloads POJOs by Kotlin data classes easily, but we’ll see that as long as they are still called by Groovy code, data classes' full power cannot be leveraged.

Call Groovy code from Kotlin with Kotlin-first compilation

You chose Kotlin-first compilation and you need to call Groovy code from Kotlin. For instance, you are migrating a Controller to Kotlin, and need to call the Service still coded in Groovy.

Assuming you’re using a dependency injection framework like Spring, all you need is an interface written in Kotlin, that will be implemented by your Groovy Service. There is an advantage to having a Kotlin interface instead of the direct call that could have been made if Groovy was compiled first: the nullability is checked at compile-time. In Groovy every parameter is nullable, the Kotlin interface allows you to define non-nullable parameters, and avoid multiple null-checks in your Groovy service.

Nothing revolutionary here I must say, however, there is another possibility that is worth considering. Does the piece of Groovy code you need really belong to this module? In our controller-service example, it definitely does. But if it is some utility method or an HTPP client, wouldn't it be better placed in an util or client module? If it is in another module, the dependency will be compiled first, and your Kotlin code will have no problem calling it. Take the chance to think about your architecture and project structure while migrating code. You could kill two birds with one stone many times, easing your code migration along with managing your dependencies better.

Keep your Groovy-generated constructors on data classes

Data classes are one of the best features of Kotlin. They are ideal for domain classes and serialized inputs and outputs of HTTP endpoints.

However, when called from Groovy, they’ll generally break one of Groovy’s key features: named parameters constructors. In Groovy, constructor invocation often looks like this:

new Person(firstName: "Thomas", lastName: "Martin", company: "Whoz")

Under the hood, Groovy generates a constructor with a map, allowing those key/value pairs as input. However, it needs a no-arguments constructor to do so.

If you transform that Person class into a Kotlin data class, you’ll get something like this:

data class Person(
val firstName: String,
val lastName: String,
val company: String?
)

When one instantiates such a data class, one needs all three parameters. This means a no-arguments constructor does not exist, so the Groovy instantiation we’ve seen sooner would not work anymore.

The solution is quite simple: defining a default value for each of the data class properties.

data class Person(
val firstName: String = "John",
val lastName: String = "Doe",
val company: String? = null
)

There’s the drawback that default values aren’t always meaningful. Storing a new Person without knowing their name and choosing John as the first name is probably not the expected behavior. We need another way of validating input, with Jackson annotations or database constraints. If you are migrating from Groovy, you probably already have those constraints. And of course, once all Person’s constructor callers are migrated to Kotlin, you can get rid of those arbitrary default values.

At Whoz, we usually make everything nullable with a null default value in those situations. It is consistent with the Groovy behavior, and it is simple to validate fields that should not be null with annotations.

Use extension functions with static equivalents for your utility methods

When you create a utility class in Groovy or Java, it is generally a bunch of static methods.

Kotlin allows you to create extension methods, really practical for utilities. So why not take the opportunity to declare such extension methods when converting a Groovy utility class? We just have to keep static methods with @JvmStaticso the Groovy code still can use them (of course Groovy cannot use extension methods).

For example:


fun Instant.toLocalDate(clock: Clock): LocalDate = InstantExtensions.toLocalDate(this, clock)

object InstantExtensions {
@JvmStatic
fun toLocalDate(instant: Instant, clock: Clock): LocalDate = LocalDateTime.ofInstant(instant, clock.zone).toLocalDate()
}
Generated with Bing image creator

Code generation with IntelliJ IDEA

Our IDE is IntelliJ, and while code generation between Groovy and Kotlin is not as easy as between Java and Kotlin, it can help as long as the limitations are well known.

To transform a Groovy class into a Kotlin one, it needs to be done in two steps:

  • Generate Java from Groovy by using Refactor > Convert to Java
  • Generate Kotlin from Java by right-clicking on the file, and selecting Convert Java File to Kotlin File

The generated file will need extensive rework. However, issues you’ll encounter are rather common, so after a couple of successfully converted files, you should be quite efficient.

Let’s see a good chunk of issues you could encounter in generated files to speed things up a little.

Casts

Groovy has its own casting methods working behind the scenes. Those simple as keywords will become ugly DefaultGroovyMethods.asType() calls.

DefaultGroovyMethods.asType(user, User::class.java)

Change it back to user as User in Kotlin.

Collections manipulations

Like Kotlin, Groovy has an important set of collection manipulation methods, such as collect , inject or findAll .

And here again, when converting to Java, DefaultGroovyMethods appears, like this:

DefaultGroovyMethods.each<Task>(
DefaultGroovyMethods.findAll<Task>(
taskService.findAllTasks(), object : Closure<Boolean?>(this, this) {
@JvmOverloads
fun doCall(it: Task? = null): Boolean {
return it!!.isCancellable
}
}), object : Closure<Task?>(this, this) {
@JvmOverloads
fun doCall(it: Task? = null): Task {
return taskService.cancel(it)
}
})

Kinda cryptic right? Let’s see the Groovy code that generated this.

taskService.findAllTasks()
.findAll { it.isCancellable() }
.each { taskService.cancel(it) }

To convert this groovy code to Kotlin, you just need to translate the collection manipulation operators. findAll becomes filter and each becomes forEach .

taskService.findAllTasks()
.filter { it.isCancellable() }
.forEach { taskService.cancel(it) }

So my advice here is to look back to the Groovy code and do the translation manually for collection manipulations.

Elvis operator

Evlis operators aren’t nicely converted by IntelliJ. Because of Groovy’s truthiness, a DefaultGroovyMethods.asBoolean() cast makes its appearance in the generated Kotlin code.

int limit = size ?: DEFAULT_LIMIT

will generate

val limit = if (DefaultGroovyMethods.asBoolean(size)) size else DEFAULT_LIMIT

instead of

val limit = size ?: DEFAULT_LIMIT

Code generation with ChatGPT

Assuming that you are allowed to send your piece of code to ChatGPT (unless it is some utility or technical method unrelated to business, we can’t do that at Whoz), it can do a pretty good job.

Let’s try with the following piece of Groovy code:

class EmailAddressListParser {

private static final String FORBIDDEN_CHARACTERS = /\s,;"'<>()@/
private static final String EMAIL_REGEX = /[^$FORBIDDEN_CHARACTERS]+@[^$FORBIDDEN_CHARACTERS]+/

static List<String> parse(String dirtyEmailAddressList) {
log.info "Analysing email address list $dirtyEmailAddressList"
List<String> identifiedEmailAddresses = []
if (dirtyEmailAddressList) {
identifiedEmailAddresses = dirtyEmailAddressList.findAll(EMAIL_REGEX).findAll { String candidate ->

EmailValidator.getInstance().isValid(candidate)
}
}
log.info "Identified email address list $identifiedEmailAddresses"
return identifiedEmailAddresses

}
}

Converted by ChatGPT:

class EmailAddressListParser {
companion object {
private const val FORBIDDEN_CHARACTERS = """\s,;"'<>()@/"""
private val EMAIL_REGEX = """[^$FORBIDDEN_CHARACTERS]+@[^$FORBIDDEN_CHARACTERS]+"""
}

fun parse(dirtyEmailAddressList: String): List<String> {
println("Analyzing email address list $dirtyEmailAddressList")
var identifiedEmailAddresses: List<String> = emptyList()
if (!dirtyEmailAddressList.isNullOrEmpty()) {
identifiedEmailAddresses = dirtyEmailAddressList.split(",").filter { candidate ->
EmailValidator.getInstance().isValid(candidate.trim())
}
}
println("Identified email address list $identifiedEmailAddresses")
return identifiedEmailAddresses
}
}

That looks good!

Be careful though to “improvements” ChatGPT will freely apply, like using println instead of a logger and trimming the email which was not done on purpose in Groovy.

Be very careful actually! ChatGPT changed the search for all EMAIL_REGEX occurrences thanks to dirtyEmailAdresses.findAll(EMAIL_REGEX) by a simple split on commas!

The same code converted by IntelliJ IDEA into Java, then Kotlin:

object EmailAddressListParser {
fun parse(dirtyEmailAddressList: String): List<String?> {
log.info("Analysing email address list $dirtyEmailAddressList")
var identifiedEmailAddresses: List<String?> = ArrayList()
if (StringGroovyMethods.asBoolean(dirtyEmailAddressList)) {
identifiedEmailAddresses = DefaultGroovyMethods.findAll(
StringGroovyMethods.findAll(dirtyEmailAddressList, EMAIL_REGEX),
object : Closure<Boolean?>(null, null) {
fun doCall(candidate: String?): Boolean {
return EmailValidator.getInstance().isValid(candidate)
}
})
}
log.info("Identified email address list $identifiedEmailAddresses")
return identifiedEmailAddresses
}

private val FORBIDDEN_CHARACTERS: String? = null
private val EMAIL_REGEX = "/" + "[^" + FORBIDDEN_CHARACTERS + "]+@[^" + FORBIDDEN_CHARACTERS + "]+" + "/"
}

It’s definitively worse, those GroovyMethods are cluttering the code, lists are initialized with ArrayList() instead of emptyList() , etc. The worst is the FORBIDDEN_CHARACTERS constant that lost its value!

Two things that are better than ChatGPT are the object declaration and the logger not downgraded to println .

Finally, this is how I would transform this code:


object EmailAddressListParser : KLogging() {

private const val FORBIDDEN_CHARACTERS = """\s,;"'<>()@/"""
private val EMAIL_REGEX = Regex("""[^$FORBIDDEN_CHARACTERS]+@[^$FORBIDDEN_CHARACTERS]+""")

fun parse(dirtyEmailAddressList: String): List<String> {
logger.info("Analyzing email address list $dirtyEmailAddressList")
val identifiedEmailAddresses = EMAIL_REGEX.findAll(dirtyEmailAddressList, 0)
.map { it.value }
.filter { EmailValidator.getInstance().isValid(it) }
.toList()

logger.info("Identified email address list $identifiedEmailAddresses")
return identifiedEmailAddresses
}
}

It is a bit more concise than ChatGPT with a proper logger and of course the use of the regex instead of the split.

ChatGPT does produce a way nicer code than IntelliJ but takes more liberties with what the code does when executed, so you also have to pay attention.

Either code generation method has its caveats, so don’t trust them much and preferably have strong unit tests.

Kodee, the Kotlin mascot

I am a Kotlin lover, maybe you’re the same and you are working on a Groovy project? Then I hope your team will be able to start introducing Kotlin into the codebase thanks to those pieces of advice.

Feel free to comment if you have other clever tricks in your mind!

--

--