Kotlin Metadata JVM library
The kotlin-metadata-jvm
library provides tools to read, modify, and generate metadata from Kotlin classes compiled for the JVM. This metadata, stored in the @Metadata
annotation within .class
files, is used by libraries and tools such as kotlin-reflect
to inspect Kotlin-specific constructs such as properties, functions, and classes at runtime.
warning
The
kotlin-reflect
library relies on metadata to retrieve Kotlin-specific class details at runtime. Any inconsistencies between the metadata and the actual.class
file may lead to incorrect behavior when using reflection.
You can also use the Kotlin Metadata JVM library to inspect various declaration attributes such as visibility or modality, or to generate and embed metadata into .class
files.
To include the Kotlin Metadata JVM library in your project, add the corresponding dependency configuration based on your build tool.
note
The Kotlin Metadata JVM library follows the same versioning as the Kotlin compiler and standard library. Ensure that the version you use matches your project's Kotlin version.
Add the following dependency to your build.gradle(.kts)
file:
// build.gradle.kts
repositories {
mavenCentral()
}
dependencies {
implementation("org.jetbrains.kotlin:kotlin-metadata-jvm:2.1.20")
}
// build.gradle
repositories {
mavenCentral()
}
dependencies {
implementation 'org.jetbrains.kotlin:kotlin-metadata-jvm:2.1.20'
}
Add the following dependency to your pom.xml
file.
<project>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-metadata-jvm</artifactId>
<version>2.1.20</version>
</dependency>
</dependencies>
...
</project>
The kotlin-metadata-jvm
library extracts structured information from compiled Kotlin .class
files, such as class names, visibility, and signatures. You can use it in projects that need to analyze compiled Kotlin declarations. For example, the Binary Compatibility Validator (BCV) relies on kotlin-metadata-jvm
to print public API declarations.
You can start exploring Kotlin class metadata by retrieving the @Metadata
annotation from a compiled class using reflection:
fun main() {
// Specifies the fully qualified name of the class
val clazz = Class.forName("org.example.SampleClass")
// Retrieves the @Metadata annotation
val metadata = clazz.getAnnotation(Metadata::class.java)
// Checks if the metadata is present
if (metadata != null) {
println("This is a Kotlin class with metadata.")
} else {
println("This is not a Kotlin class.")
}
}
After retrieving the @Metadata
annotation, use either the readLenient()
or the readStrict()
function from the KotlinClassMetadata
API to parse it. These functions extract detailed information about classes or files, while addressing different compatibility requirements:
readLenient()
: Use this function to read metadata, including metadata generated by newer Kotlin compiler versions. This function doesn't support modifying or writing metadata.readStrict()
: Use this function when you need to modify and write metadata. ThereadStrict()
function only works with metadata generated by Kotlin compiler versions fully supported by your project.note
The
readStrict()
function supports metadata formats up to one version beyondJvmMetadataVersion.LATEST_STABLE_SUPPORTED
, which corresponds to the latest Kotlin version used in the project. For example, if your project depends onkotlin-metadata-jvm:2.1.0
,readStrict()
can process metadata up to Kotlin2.2.x
; otherwise, it throws an error to prevent mishandling unknown formats.For more information, see the Kotlin Metadata GitHub repository.
When parsing metadata, the KotlinClassMetadata
instance provides structured information about class or file-level declarations. For classes, use the kmClass
property to analyze detailed class-level metadata, such as the class name, functions, properties, and attributes like visibility. For file-level declarations, the metadata is represented by the kmPackage
property, which includes top-level functions and properties from file facades generated by the Kotlin compiler.
The following code example demonstrates how to use readLenient()
to parse metadata, analyze class-level details with kmClass
, and retrieve file-level declarations with kmPackage
:
// Imports the necessary libraries
import kotlin.metadata.jvm.*
import kotlin.metadata.*
fun main() {
// Specifies the fully qualified class name
val className = "org.example.SampleClass"
try {
// Retrieves the class object for the specified name
val clazz = Class.forName(className)
// Retrieves the @Metadata annotation
val metadataAnnotation = clazz.getAnnotation(Metadata::class.java)
if (metadataAnnotation != null) {
println("Kotlin Metadata found for class: $className")
// Parses metadata using the readLenient() function
val metadata = KotlinClassMetadata.readLenient(metadataAnnotation)
when (metadata) {
is KotlinClassMetadata.Class -> {
val kmClass = metadata.kmClass
println("Class name: ${kmClass.name}")
// Iterates over functions and checks visibility
kmClass.functions.forEach { function ->
val visibility = function.visibility
println("Function: ${function.name}, Visibility: $visibility")
}
}
is KotlinClassMetadata.FileFacade -> {
val kmPackage = metadata.kmPackage
// Iterates over functions and checks visibility
kmPackage.functions.forEach { function ->
val visibility = function.visibility
println("Function: ${function.name}, Visibility: $visibility")
}
}
else -> {
println("Unsupported metadata type: $metadata")
}
}
} else {
println("No Kotlin Metadata found for class: $className")
}
} catch (e: ClassNotFoundException) {
println("Class not found: $className")
} catch (e: Exception) {
println("Error processing metadata: ${e.message}")
e.printStackTrace()
}
}
While you can retrieve metadata using reflection, another approach is to extract it from bytecode using a bytecode manipulation framework such as ASM.
You can do this by following these steps:
Read the bytecode of a
.class
file using the ASM library'sClassReader
class. This class processes the compiled file and populates aClassNode
object, which represents the class structure.Extract the
@Metadata
from theClassNode
object. The example below uses a custom extension functionfindAnnotation()
for this.Parse the extracted metadata using the
KotlinClassMetadata.readLenient()
function.Inspect the parsed metadata with the
kmClass
andkmPackage
properties.
Here's an example:
// Imports the necessary libraries
import kotlin.metadata.jvm.*
import kotlin.metadata.*
import org.objectweb.asm.*
import org.objectweb.asm.tree.*
import java.io.File
// Checks if an annotation refers to a specific name
fun AnnotationNode.refersToName(name: String) =
desc.startsWith('L') && desc.endsWith(';') && desc.regionMatches(1, name, 0, name.length)
// Retrieves annotation values by key
private fun List<Any>.annotationValue(key: String): Any? {
for (index in (0 until size / 2)) {
if (this[index * 2] == key) {
return this[index * 2 + 1]
}
}
return null
}
// Defines a custom extension function to locate an annotation by its name in a ClassNode
fun ClassNode.findAnnotation(annotationName: String, includeInvisible: Boolean = false): AnnotationNode? {
val visible = visibleAnnotations?.firstOrNull { it.refersToName(annotationName) }
if (!includeInvisible) return visible
return visible ?: invisibleAnnotations?.firstOrNull { it.refersToName(annotationName) }
}
// Operator to simplify retrieving annotation values
operator fun AnnotationNode.get(key: String): Any? = values.annotationValue(key)
// Extracts Kotlin metadata from a class node
fun ClassNode.readMetadataLenient(): KotlinClassMetadata? {
val metadataAnnotation = findAnnotation("kotlin/Metadata", false) ?: return null
@Suppress("UNCHECKED_CAST")
val metadata = Metadata(
kind = metadataAnnotation["k"] as Int?,
metadataVersion = (metadataAnnotation["mv"] as List<Int>?)?.toIntArray(),
data1 = (metadataAnnotation["d1"] as List<String>?)?.toTypedArray(),
data2 = (metadataAnnotation["d2"] as List<String>?)?.toTypedArray(),
extraString = metadataAnnotation["xs"] as String?,
packageName = metadataAnnotation["pn"] as String?,
extraInt = metadataAnnotation["xi"] as Int?
)
return KotlinClassMetadata.readLenient(metadata)
}
// Converts a file to a ClassNode for bytecode inspection
fun File.toClassNode(): ClassNode {
val node = ClassNode()
this.inputStream().use { ClassReader(it).accept(node, ClassReader.SKIP_CODE) }
return node
}
fun main() {
val classFilePath = "build/classes/kotlin/main/org/example/SampleClass.class"
val classFile = File(classFilePath)
// Reads the bytecode and processes it into a ClassNode object
val classNode = classFile.toClassNode()
// Locates the @Metadata annotation and reads it leniently
val metadata = classNode.readMetadataLenient()
if (metadata != null && metadata is KotlinClassMetadata.Class) {
// Inspects the parsed metadata
val kmClass = metadata.kmClass
// Prints class details
println("Class name: ${kmClass.name}")
println("Functions:")
kmClass.functions.forEach { function ->
println("- ${function.name}, Visibility: ${function.visibility}")
}
}
}
When using tools like ProGuard to shrink and optimize bytecode, some declarations may be removed from .class
files. ProGuard automatically updates metadata to keep it consistent with the modified bytecode.
However, if you're developing a custom tool that modifies Kotlin bytecode in a similar way, you need to ensure that metadata is adjusted accordingly. With the kotlin-metadata-jvm
library, you can update declarations, adjust attributes, and remove specific elements.
For example, if you use a JVM tool that deletes private methods from Java class files, you must also delete private functions from Kotlin metadata to maintain consistency:
Parse the metadata by using the
readStrict()
function to load the@Metadata
annotation into a structuredKotlinClassMetadata
object.Apply modifications by adjusting the metadata, such as filtering functions or altering attributes, directly within
kmClass
or other metadata structures.Use the
write()
function to encode the modified metadata into a new@Metadata
annotation.
Here's an example where private functions are removed from a class's metadata:
// Imports the necessary libraries
import kotlin.metadata.jvm.*
import kotlin.metadata.*
fun main() {
// Specifies the fully qualified class name
val className = "org.example.SampleClass"
try {
// Retrieves the class object for the specified name
val clazz = Class.forName(className)
// Retrieves the @Metadata annotation
val metadataAnnotation = clazz.getAnnotation(Metadata::class.java)
if (metadataAnnotation != null) {
println("Kotlin Metadata found for class: $className")
// Parses metadata using the readStrict() function
val metadata = KotlinClassMetadata.readStrict(metadataAnnotation)
if (metadata is KotlinClassMetadata.Class) {
val kmClass = metadata.kmClass
// Removes private functions from the class metadata
kmClass.functions.removeIf { it.visibility == Visibility.PRIVATE }
println("Removed private functions. Remaining functions: ${kmClass.functions.map { it.name }}")
// Serializes the modified metadata back
val newMetadata = metadata.write()
// After modifying the metadata, you need to write it into the class file
// To do so, you can use a bytecode manipulation framework such as ASM
println("Modified metadata: ${newMetadata}")
} else {
println("The metadata is not a class.")
}
} else {
println("No Kotlin Metadata found for class: $className")
}
} catch (e: ClassNotFoundException) {
println("Class not found: $className")
} catch (e: Exception) {
println("Error processing metadata: ${e.message}")
e.printStackTrace()
}
}
tip
Instead of separately calling
readStrict()
andwrite()
, you can use thetransform()
function. This function parses metadata, applies transformations through a lambda, and writes the modified metadata automatically.
To create metadata for a Kotlin class file from scratch using the Kotlin Metadata JVM library:
Create an instance of
KmClass
,KmPackage
, orKmLambda
, depending on the type of metadata you want to generate.Add attributes to the instance, such as the class name, visibility, constructors, and function signatures.
tip
You can use the
apply()
scope function to reduce boilerplate code while setting properties.Use the instance to create a
KotlinClassMetadata
object, which can generate a@Metadata
annotation.Specify the metadata version, such as
JvmMetadataVersion.LATEST_STABLE_SUPPORTED
, and set flags (0
for no flags, or copy flags from existing files if necessary).Use the
ClassWriter
class from ASM to embed metadata fields, such askind
,data1
anddata2
into a.class
file.
The following example demonstrates how to create metadata for a simple Kotlin class:
// Imports the necessary libraries
import kotlin.metadata.*
import kotlin.metadata.jvm.*
import org.objectweb.asm.*
fun main() {
// Creates a KmClass instance
val klass = KmClass().apply {
name = "Hello"
visibility = Visibility.PUBLIC
constructors += KmConstructor().apply {
visibility = Visibility.PUBLIC
signature = JvmMethodSignature("<init>", "()V")
}
functions += KmFunction("hello").apply {
visibility = Visibility.PUBLIC
returnType = KmType().apply {
classifier = KmClassifier.Class("kotlin/String")
}
signature = JvmMethodSignature("hello", "()Ljava/lang/String;")
}
}
// Serializes a KotlinClassMetadata.Class instance, including the version and flags, into a @kotlin.Metadata annotation
val annotationData = KotlinClassMetadata.Class(
klass, JvmMetadataVersion.LATEST_STABLE_SUPPORTED, 0
).write()
// Generates a .class file with ASM
val classBytes = ClassWriter(0).apply {
visit(Opcodes.V1_6, Opcodes.ACC_PUBLIC, "Hello", null, "java/lang/Object", null)
// Writes @kotlin.Metadata instance to the .class file
visitAnnotation("Lkotlin/Metadata;", true).apply {
visit("mv", annotationData.metadataVersion)
visit("k", annotationData.kind)
visitArray("d1").apply {
annotationData.data1.forEach { visit(null, it) }
visitEnd()
}
visitArray("d2").apply {
annotationData.data2.forEach { visit(null, it) }
visitEnd()
}
visitEnd()
}
visitEnd()
}.toByteArray()
// Writes the generated class file to disk
java.io.File("Hello.class").writeBytes(classBytes)
println("Metadata and .class file created successfully.")
}
tip
For a more detailed example, see the Kotlin Metadata JVM GitHub repository.
Thanks for your feedback!