Kotlin Value Classes and Mangled Handlebars ViewModels

July 18, 2024

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 {{name}}

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: Side-by-side of code and bytecode 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)

Unmangled getter in bytecode

And now Handlebars should render Hello Kristian.