Skip to content

getJvmName returns incorrect accessor names for @JvmRecord data classes #2812

@MariusVolkhart

Description

@MariusVolkhart

Summary

Resolver.getJvmName(accessor) returns getName for a property getter on a @JvmRecord Kotlin data class, but the actual JVM bytecode uses record component accessors (name(), not getName()). This causes downstream frameworks (e.g. Micronaut Data) that rely on getJvmName to generate incorrect reflective calls, resulting in NoSuchMethodError at runtime.

Environment

  • KSP version: 2.2.21-2.0.5 (also reproducible on main at 2.3.0)
  • Kotlin version: 2.2.21+
  • JDK: 17+

Reproducer

Given this Kotlin source:

@JvmRecord
data class Author(
    val id: Int,
    val name: String,
)

A KSP processor calling:

val cls = resolver.getClassDeclarationByName("Author")!!
val nameGetter = cls.getAllProperties().first { it.simpleName.asString() == "name" }.getter!!
val jvmName = resolver.getJvmName(nameGetter)
println(jvmName) // prints "getName" — should print "name"

Expected behavior

resolver.getJvmName(getter) should return "name" (the record component accessor), matching the actual JVM bytecode produced by the Kotlin compiler for @JvmRecord classes.

Actual behavior

Returns "getName" (the bean-style getter), which does not exist on the compiled Java record class.

Root cause

In ResolverAAImpl.getJvmName(accessor: KSPropertyAccessor) (line 445 of ResolverAAImpl.kt), the name is computed as:

val name = accessor.receiver.simpleName.asString()
val prefixedName = when (accessor) {
    is KSPropertyGetter -> JvmAbi.getterName(name)   // unconditionally → "getName"
    is KSPropertySetter -> JvmAbi.setterName(name)
    else -> ""
}

JvmAbi.getterName() unconditionally adds a get prefix. There is no check for whether the containing class is a @JvmRecord class, which suppresses the prefix at the JVM level.

Why KSP1 worked correctly

The KSP1 implementation (ResolverImpl) delegated to typeMapper.mapFunctionName(descriptor, OwnerKind.IMPLEMENTATION), which is the Kotlin compiler's internal KotlinTypeMapper. That function explicitly checks for java.lang.Record supertypes and returns the bare property name when the class is a record. KSP2 replaced this with a manual JvmAbi.getterName() call and lost the record-awareness.

Impact

Any KSP processor that uses getJvmName to determine how to reflectively access properties on @JvmRecord classes will get the wrong method name. Known affected framework: Micronaut Data's micronaut-inject-kotlin processor, which generates bean introspection code using getJvmName — causing NoSuchMethodError at runtime for all @JvmRecord entities.

Proposed fix

In ResolverAAImpl.getJvmName(accessor), before computing the prefixed name, check whether the containing class has the kotlin.jvm.JvmRecord annotation. If so, return the bare property name (matching the Kotlin compiler's bytecode output):

// Annotation classes and @JvmRecord data classes both use bare property names
// as accessor names (no get/set prefix).
if (containingClass?.classKind == ClassKind.ANNOTATION_CLASS ||
    containingClass != null && containingClass.annotations.any {
        it.annotationType.resolve().declaration.qualifiedName?.asString() == "kotlin.jvm.JvmRecord"
    }
) {
    return name
}

For setters, records don't have setters at all (record components are final), so the setter branch is unaffected.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions