Kotlin Help

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.

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.

Add the library to your project

To include the Kotlin Metadata JVM library in your project, add the corresponding dependency configuration based on your build tool.

Gradle

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' }

Maven

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>

Read and parse metadata

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. The readStrict() function only works with metadata generated by Kotlin compiler versions fully supported by your project.

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() } }

Extract metadata from bytecode

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:

  1. Read the bytecode of a .class file using the ASM library's ClassReader class. This class processes the compiled file and populates a ClassNode object, which represents the class structure.

  2. Extract the @Metadata from the ClassNode object. The example below uses a custom extension function findAnnotation() for this.

  3. Parse the extracted metadata using the KotlinClassMetadata.readLenient() function.

  4. Inspect the parsed metadata with the kmClass and kmPackage 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}") } } }

Modify metadata

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:

  1. Parse the metadata by using the readStrict() function to load the @Metadata annotation into a structured KotlinClassMetadata object.

  2. Apply modifications by adjusting the metadata, such as filtering functions or altering attributes, directly within kmClass or other metadata structures.

  3. 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() } }

Create metadata from scratch

To create metadata for a Kotlin class file from scratch using the Kotlin Metadata JVM library:

  1. Create an instance of KmClass, KmPackage, or KmLambda, depending on the type of metadata you want to generate.

  2. Add attributes to the instance, such as the class name, visibility, constructors, and function signatures.

  3. Use the instance to create a KotlinClassMetadata object, which can generate a @Metadata annotation.

  4. 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).

  5. Use the ClassWriter class from ASM to embed metadata fields, such as kind, data1 and data2 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.") }

What's next

Last modified: 04 April 2025