Annotations & Reflection

Reviewed & published by Brayan K

Annotations are metadata labels you attach to code, and reflection is the ability of a program to inspect its own classes, functions, and properties at runtime.

Part of the free Kotlin course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

In this lesson you'll declare your own annotations, use built-in ones like @Deprecated and @JvmStatic, and read type information with ::class and KClass.

What You'll Learn in This Lesson

1️⃣ Declaring and Applying Annotations

You declare an annotation with the annotation class keyword. Its constructor parameters become the data the annotation carries. You then apply it with an @ in front of a declaration. Crucially, an annotation does nothing on its own — it just sits there as metadata until a tool, framework, or reflection reads it.

// An annotation is just metadata attached to code.
// You declare one with the 'annotation class' keyword.
annotation class Reviewed(val by: String, val score: Int)

// Apply it like a label on top of a declaration:
@Reviewed(by = "Ada", score = 5)
fun deploy() {
    println("Deploying...")
}

fun main() {
    // Annotations do NOTHING on their own — they only carry data.
    // Calling the function still works exactly as normal:
    deploy()
    println("Done")
}

2️⃣ Built-in Annotations

Kotlin ships with useful annotations. @Deprecated marks a declaration as outdated and produces a compiler warning (with an optional ReplaceWith auto-fix). @JvmStatic makes a companion-object member callable as a real static from Java. Others you'll meet include @JvmOverloads and @Throws.

// Kotlin ships with handy built-in annotations.
// @Deprecated warns anyone who still calls the old function.
@Deprecated("Use greet(name) instead", ReplaceWith("greet(name)"))
fun sayHi(name: String) = "Hi $name"

fun greet(name: String) = "Hello, $name!"

fun main() {
    // The old one still RUNS — @Deprecated is only a compiler warning:
    println(sayHi("Ada"))
    println(greet("Ada"))
}

Notice the deprecated function still executes — annotations don't block code, they inform the compiler and tooling. The IDE will draw a strikethrough over sayHi and offer to replace it.

3️⃣ Reflection with ::class and KClass

Reflection lets your program inspect itself while running. The ::class reference gives you a KClass — an object describing a type. From an instance use value::class; from a type use TypeName::class. A KClass exposes simpleName, qualifiedName, and members.

// Reflection lets a program inspect itself at runtime.
// ::class gives you a KClass describing the type.
class Robot(val name: String, val battery: Int)

fun main() {
    val r = Robot("R2D2", 87)

    // Get the KClass from an instance with ::class
    val kClass = r::class

    // simpleName is just the class name as a String:
    println(kClass.simpleName)        // -> Robot

    // You can also get a KClass from a type with Robot::class
    println(Robot::class.simpleName)  // -> Robot

    // Property names (sorted so the output is stable):
    val names = kClass.members
        .map { it.name }
        .filter { it == "name" || it == "battery" }
        .sorted()
    println(names)                    // -> [battery, name]
}

We sorted the property names so the output is predictable. In real code, full members reflection on the JVM needs the kotlin-reflect dependency.

Your turn. Fill in the ___ blanks, then run and compare.

// 🎯 YOUR TURN — replace each ___ then run main()
annotation class Tag(val label: String)

@Tag("important")
class Note(val text: String)

fun main() {
    val n = Note("buy milk")

    // 1) Get the KClass of n using ::class
    val kClass = n.___          // 👉 put: class

    // 2) Print the simple name of the class
    println(kClass.___)         // 👉 put: simpleName

    // ✅ Expected output:
    // Note
}

Mini-Challenge: Annotate and Inspect

Declare a no-parameter annotation, apply it to a class, then use reflection to print that class's name.

// 🎯 MINI-CHALLENGE: a tiny annotation + reflection check.
// Save as Audit.kt and run main().
//
// 1. Declare an annotation class Important (no parameters).
// 2. Apply @Important to a class called Payment.
// 3. In main(), create a Payment and print its class simpleName.
//
// ✅ Expected output:
//    Payment

annotation class Important

@Important
class Payment(val cents: Int)

fun main() {
    val p = Payment(500)
    // your code here: print the class simpleName
}

Common Errors (and the fix)

Pro Tips

📋 Quick Reference — Annotations & Reflection

ConceptKotlin Syntax
Declare annotationannotation class Tag(val s: String)
Apply it@Tag("x") class Foo
Restrict targets@Target(AnnotationTarget.CLASS)
Keep for reflection@Retention(AnnotationRetention.RUNTIME)
KClass from instanceobj::class
Class namekClass.simpleName

🎉 Lesson Complete!

Practice quiz

What is an annotation in Kotlin?

  • A type of loop
  • A way to throw exceptions
  • Metadata attached to code that does nothing on its own
  • A collection operator

Answer: Metadata attached to code that does nothing on its own. An annotation carries metadata; it only matters when a tool or reflection reads it.

Which keyword declares your own annotation?

  • annotation class
  • data class
  • object
  • sealed class

Answer: annotation class. You declare one with the annotation class keyword.

What does @Deprecated do to a function it marks?

  • Deletes the function
  • Makes it private
  • Stops it from compiling
  • Produces a compiler warning but the function still runs

Answer: Produces a compiler warning but the function still runs. @Deprecated only warns; the deprecated function still executes.

What does @Target control for an annotation?

  • How long it survives
  • Where the annotation may be applied
  • Its default arguments
  • Whether it is public

Answer: Where the annotation may be applied. @Target lists the AnnotationTargets allowed, like CLASS or FUNCTION.

Which @Retention is needed so reflection can read an annotation at runtime?

  • RUNTIME
  • SOURCE
  • BINARY
  • COMPILE

Answer: RUNTIME. RUNTIME keeps the annotation available so reflection can read it.

How do you get a KClass from an instance named r?

  • r.getClass()
  • KClass(r)
  • r::class
  • r.kclass

Answer: r::class. Use the ::class reference on the instance, e.g. r::class.

What does KClass.simpleName return?

  • The full package path
  • The class name as a String
  • The number of members
  • A list of properties

Answer: The class name as a String. simpleName is just the class name; qualifiedName includes the package.

What value must an annotation argument be?

  • Any runtime variable
  • A lambda
  • A mutable list
  • A compile-time constant such as a literal or const val

Answer: A compile-time constant such as a literal or const val. Annotation arguments must be compile-time constants.

When is reflection most appropriate?

  • For all ordinary application logic
  • When you genuinely cannot know the types ahead of time
  • To replace every interface
  • To speed up loops

Answer: When you genuinely cannot know the types ahead of time. Reflection is slow and bypasses safety; use it only when types are unknown ahead of time.

What does @JvmStatic do?

  • Makes a function run faster in Kotlin
  • Marks a class as final
  • Generates a real JVM static member for Java interop
  • Adds null safety

Answer: Generates a real JVM static member for Java interop. @JvmStatic helps Java call companion members as real statics; no effect from Kotlin.

Continue this course

Frequently asked questions

What exactly is an annotation in Kotlin?

An annotation is metadata — a label you attach to a class, function, property, or parameter that carries extra information without changing how the code runs. You declare one with 'annotation class Name(val x: Type)' and apply it with @Name(...). By itself an annotation does nothing; its value comes from tools, frameworks, or reflection that read it. Examples include @Deprecated (a compiler warning), @Test (read by JUnit to find tests), and @JvmStatic (read by the compiler to change how a method is generated for Java).

What do @Target and @Retention control?

@Target lists where your annotation may be applied — for example @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) restricts it to classes and functions, so the compiler rejects misuse elsewhere. @Retention controls how long the annotation survives: SOURCE means it is discarded after compilation (useful for lint-style markers), BINARY keeps it in the .class file, and RUNTIME (the default) keeps it available so reflection can read it at runtime. If you plan to inspect an annotation with reflection, it must have RUNTIME retention.

What is reflection and what is a KClass?

Reflection is the ability of a program to examine and manipulate its own structure at runtime — to ask questions like 'what is this object's type?' or 'what properties does this class have?'. In Kotlin the entry point is KClass, obtained with ::class (for example r::class from an instance, or Robot::class from a type). A KClass exposes things like simpleName, qualifiedName, and members. Working with reflection requires the kotlin-reflect library on the JVM for full functionality.

When is reflection actually worth using?

Reflection is powerful but slow and it bypasses compile-time safety, so reach for it only when you genuinely cannot know the types ahead of time: serialization libraries mapping JSON to objects, dependency-injection frameworks wiring up components, test runners discovering @Test methods, or ORMs reading table mappings. For ordinary application logic prefer plain method calls, interfaces, sealed classes, or generics — they are faster, safer, and clearer. A good rule of thumb: if you can solve it without reflection, do.

What does @JvmStatic do?

@JvmStatic is a built-in annotation placed on a function or property inside a companion object (or a named object). It tells the Kotlin compiler to also generate a genuine static member at the JVM level, so Java callers can write MyClass.method() instead of MyClass.Companion.method(). It has no effect when you call the member from Kotlin — it exists purely to make the generated bytecode friendlier to Java interop. It is one of several @Jvm* annotations covered more fully in the Java Interop lesson.