Kotlin Value Classes and Mangled Handlebars ViewModels
TL;DR:
@get:JvmName("getName") val name: Username
I recently started a project using Kotlin, Http4K and Handlebars. Setting this up and using it was quite straightforward:
// File: MyWebpage.kt
private val renderer = HandlebarsTemplates().CachingClasspath("templates")
private val bodyLens = Body.viewModel(renderer, ContentType.TEXT_HTML).toLens()
data class MyWebpage(val name: Username) : ViewModel
@JvmInline
value class Username(val value: String)
fun myEndpoint(request: Request): Response {
return Response(Status.OK)
.with(bodyLens of MyWebpage(name = Username("Kristian")))
}
and the template:
Hello
🔗 And this works great!
Except, it doesn't.
If you run this code, Handlebars will mention that it can't find the helper name
(given you enable logging or set a missingHelper
).
How come?
Let's change the MyWebpage
data class a tiny bit:
data class MyWebpage(val name: String) : ViewModel
This time, it works! For real! So the value class
is somehow screwing things up. Could it be that your template needs to say {{name.value}}
to read inside the value class?
Nope! Same issue.
🔗 It's never a compiler bug
And no, it's not a compiler bug. But it is the compiler. You see, Handlebars is written in Java and looks in your ViewModel
instance (MyWebpage
) with getDeclaredMethods (reflection).
Then it follows regular POJO rules, so when your template is referencing a {{name}}
, Handlebars will look for a getName
method.
When we use val name: String
with the String
type, this method exists, because kotlin adds it for Java interop, along with a private backing field.
But how come Handlebars can't find any getName(): String
method when we use a value class
?
🔗 Enter: Mangling
Kotlin has to do some smart tricks when you use value classes, to avoid creating methods that collide in the Java world.
As an example, the two methods getName(): String
and getName(): Username
are exactly the same, because we @JvmInline
this Username
class, and therefore replace any occurence of Username
in bytecode with String
.
So in Java, your kotlin code looks like getName(): String
twice. And that's an issue! 💣
To solve this, the Kotlin compiler applies mangling to the generated getter. (Digression: If you've used python before, perhaps you remember double underscore for __my_internal_field
).
The method generated in bytecode is actually called: getName-D0aAuEE(): String
.
You can see this by using the Bytecode Viewer or Show bytecode actions in IntelliJ:
🔗 So how do we fix this?
I obviously do not want to edit my Handlebars template to say {{name-D0aAuEE}}
.
So instead, we alter the generated bytecode using an annotation.
IntelliJ annotations on properties can affect the property itself, the backing field, or the getter or setter. In this case, we only care about the getter.
The annotation is targeting the getter using the @get:
syntax.
To rename the generated method to a normal POJO style, Kotlin has a JvmName("newName")
annotation.
So, to fix our issues, all we have to do (on every field of value class
inside MyWebpage
):
data class MyWebpage(@get:JvmName("getName") val name: Username)
And now Handlebars should render Hello Kristian
.