Skip to main content

Mangled JVM names in public API

MANGLED_JVM_NAME_PUBLIC_API reports public functions, properties, and constructors that Java sources can't call because a value class in their signature makes the JVM backend mangle the compiled name.

DiagnosticMANGLED_JVM_NAME_PUBLIC_API
Default severityError
Applies toJVM compilations only
Gradle propertymangledJvmNamePublicApi
Exemption@IntentionallyMangledJvmName

What it reports

A value class among the value parameters, the extension receiver, or the context parameters of a function or property - nullable types and type parameters bounded by a value class included.

@file:JvmName("Accounts")
// Supporting value class
/**
* Stable identifier assigned to an account.
*
* @property raw identifier as stored by the account service.
*/
@JvmInline
public value class AccountId(public val raw: String)
/** Finds the account identified by [id]. */
public fun find(id: AccountId) { }

Rationale

Value classes compile to their underlying type, so the backend needs a hashed suffix to keep the compiled method distinct from an overload taking the underlying type directly - take(id: UserId) compiles to take-4ZD5Yi0(...). A constructor gets no such suffix: the visible one becomes private and a synthetic overload with a marker parameter takes its place. Kotlin callers resolve by the source signature and never notice, but for Java the declaration is unreachable. See the Kotlin guide on inline value classes and mangling.

Don't

@file:JvmName("Users")
// Compiles to take-4ZD5Yi0(...): an illegal Java identifier.
/** Queues a refresh for the account identified by [id]. */
public fun take(id: UserId) { }

Do

@file:JvmName("Users")
/** Queues a refresh for the account identified by [id]. */
@JvmName("take")
public fun take(id: UserId) { }

Don't

// The public constructor is replaced by
// a private one and a synthetic marker-parameter overload.
/**
* Wallet associated with a user account.
*
* @property id identifier of the account that owns the wallet.
*/
@Poko
public class Wallet(public val id: UserId)

Do

/**
* Wallet associated with a user account.
*
* @property id identifier of the account that owns the wallet.
*/
@OptIn(ExperimentalStdlibApi::class)
@JvmExposeBoxed
@Poko
public class Wallet(public val id: UserId)

@JvmExposeBoxed generates Java-callable boxed variants alongside the mangled ones. It is the only fix for constructors and overridable members, since @JvmName doesn't accept them.

Notes

  • A value class inside a type argument (List<UserId>) is boxed and keeps the JVM name. Only the classifier itself mangles, not a type it is nested in.
  • A top-level callable that merely returns a value class keeps its JVM name. The return type only mangles for members, where the dispatch receiver makes the difference.
  • A var property's setter mangles independently of the getter - renaming or hiding one accessor leaves the other checked on its own.
  • Members and constructors of the value class itself are not reported: declaring the public value class is the deliberate choice, and @JvmName is not even applicable inside it.
  • suspend functions are not reported: an unmangled name would not make them Java-callable anyway.
  • Overrides are not reported: their signature is fixed by the overridden declaration, which is reported instead.
  • @PublishedApi internal declarations are not reported: their public bytecode entry is a binary implementation detail, not supported Java source API.
  • @JvmSynthetic declarations are hidden from Java on purpose and are not reported.
  • Non-JVM compilations never register this check at all.

Exemption

Apply @IntentionallyMangledJvmName when Java callers are not supported for this declaration:

@file:JvmName("Users")
/** Queues a refresh for [id] through a deliberately Kotlin-only API. */
@IntentionallyMangledJvmName(reason = ExemptionReason.API_DESIGN)
public fun take(id: UserId) { }

Configuration

apiWatchdog {
javaInterop {
mangledJvmNamePublicApi = WatchdogSeverity.WARNING
}
}

The property lives inside the javaInterop { } block. javaInterop { enabled = false } turns off this check along with the rest of the Java interop checks group.

With direct compiler invocation:

-P plugin:org.jetbrains.kotlin.library.api.watchdog:diagnosticSeverity=MANGLED_JVM_NAME_PUBLIC_API:warning

See also