JavaRush /Courses /Kotlin SELF /Reflective access to properties and functions in Kotlin

Reflective access to properties and functions in Kotlin

Kotlin SELF
Level 61, Lesson 1
Available

1. Introduction

Usually we call code “statically”: we write expense.amount or expense.label() —and the compiler guarantees that such members exist and the types match. But sometimes you want to build something like an “inspector”: print all fields of an object, write a universal debugging command in a CLI, support plugins, or do serialization “without knowing the types in advance”. In those cases, reflection is like a flashlight in a dark room: not required all the time, but sometimes it saves you.

It’s important to accept this upfront: reflection is almost always more fragile. Often you search for a property by a string "amount" or a method by a string "label", and any typo turns into not a compile-time error, but a runtime “why can’t it be found?”. So today we’ll learn not only “how to find and call”, but how to do it predictably: with checks, clear fallbacks, and proper error messages.

2. Adding kotlin-reflect

If you try to use Kotlin’s “rich” reflection (for example, memberProperties), you may run into a surprise: “imports are there, but the project won’t build” or “something can’t be found at runtime”. The reason is that full-featured reflection is its own thing, and in the JVM world it is often shipped as a separate library/artifact. Historically, even Kotlin CLI commands had options related to whether kotlin-reflect is included in the classpath, for example -no-reflect.

Practically, for us this means a simple rule: if you want to use the API from kotlin.reflect.full.*, be ready to add the kotlin-reflect dependency in Gradle. A Kotlin project includes different parts of the standard ecosystem, and kotlin-reflect is one of the libraries in that “bundle”.

A mini example of what the setup looks like (Gradle Kotlin DSL syntax):

dependencies {
    implementation(kotlin("reflect"))
}

And in code you typically need imports roughly like this:

import kotlin.reflect.full.memberFunctions
import kotlin.reflect.full.memberProperties

If you see that memberProperties “can’t be found” or IDEA complains about imports, your first thought should not be “reflection is broken”, but “looks like kotlin-reflect isn’t added”.

3. Properties via memberProperties

Getting the list of properties

Once we can get a KClass, the next logical question is: “OK, what properties does this class have?”. In Kotlin, you do this via memberProperties. This is not magic: essentially, you ask the KClass for a list of property descriptors, and then you can work with it like a regular collection—filter, search by name, print.

Since it is a collection, you almost automatically start using operations like map { it.name } —and that’s good: the code becomes shorter and more readable. By the way, these collection transformations are a core part of the Kotlin standard library.

Let’s sketch a minimal example on our “practical” mini topic—expense tracking. Suppose we have a model:

data class Expense(
    val id: Int,
    val title: String,
    val amount: Double
)

Now let’s get the property names:

import kotlin.reflect.full.memberProperties

fun main() {
    val names = Expense::class.memberProperties.map { it.name }
    println(names) // [amount, id, title] (order may differ)
}

It’s important to get used to this idea: the order of properties in the output is not a contract of your application. This is metadata, and relying on “title will always be first” is a bad idea. If order matters, you sort it explicitly yourself (for example, sorted()), instead of expecting that “this is how Kotlin does it”.

Reading a property value via get(receiver)

A list of properties by itself is just “descriptions”. To get the value, you need an object (receiver): a specific Expense from which we read title, amount, and so on. In reflection it looks like this: every property has a getter, and you call property.get(obj).

Let’s start simple: print all properties and their values. This is very similar to the “debug print” we want to embed into our CLI app.

import kotlin.reflect.full.memberProperties

data class Expense(val id: Int, val title: String, val amount: Double)

fun main() {
    val e = Expense(id = 1, title = "Coffee", amount = 3.5)

    for (p in Expense::class.memberProperties) {
        println("${p.name} = ${p.get(e)}")
        // id = 1
        // title = Coffee
        // amount = 3.5
    }
}

The key point here: p.get(e) means “read the value of this property from object e”. If you forget to pass the receiver, the compiler won’t allow it—and that’s a good hint: “without an object you’re not reading a value, you’re holding the idea of a value”.

Searching for a property by name

As soon as you try to do “string-based access”, you get the classic risk: the property may not exist. Also, it may be spelled almost the same but not quite (for example, "ammount" instead of "amount"—one letter, and hello, bug).

That’s why when we search by name, the “safe Kotlin” style looks like this: firstOrNull { ... }, then ?., then a clear fallback. This is exactly the pattern we have used many times for strings, collections, and nullable types.

Let’s make a utility readPropertyOrNull: it tries to read a property by name and returns Any?, and if it fails, it returns null.

import kotlin.reflect.full.memberProperties

fun readPropertyOrNull(obj: Any, propName: String): Any? {
    val p = obj::class.memberProperties.firstOrNull { it.name == propName }
    return p?.get(obj)
}

Let’s test it:

data class Expense(val id: Int, val title: String, val amount: Double)

fun main() {
    val e = Expense(1, "Coffee", 3.5)

    println(readPropertyOrNull(e, "title"))   // Coffee
    println(readPropertyOrNull(e, "oops"))    // null
}

If you’re thinking: “Why not use first { ... }?”—because first { ... } throws an exception when the element isn’t found. But a missing property by string is usually an expected situation, not “the end of the world”. We want controlled behavior, not a sudden crash.

4. Functions via memberFunctions

Invoking a method via call(...)

Access to functions works similarly: from a KClass you can get memberFunctions, find the one you need by name, and invoke it. In Kotlin reflection, invocation is done via call(...). Sounds simple, but there are two “practical” nuances that beginners most often trip over.

First nuance: a member function needs a receiver (the object you call the method on). That is, call(obj, ...), not just call(...).

Second nuance: a function has parameters, and reflection won’t guess for you how many arguments you need to pass. So before call, it’s useful to check “how many parameters are expected?”.

Let’s add a couple of methods to Expense:

data class Expense(val id: Int, val title: String, val amount: Double) {
    fun label(): String = "$title: $amount"
    fun withTax(percent: Int): Double = amount * (1 + percent / 100.0)
}

Now let’s find and call label():

import kotlin.reflect.full.memberFunctions

fun main() {
    val e = Expense(1, "Coffee", 3.5)

    val fn = e::class.memberFunctions.firstOrNull { it.name == "label" }
    val result = fn?.call(e)

    println(result) // Coffee: 3.5
}

And now call withTax(percent: Int):

import kotlin.reflect.full.memberFunctions

fun main() {
    val e = Expense(1, "Coffee", 3.5)

    val fn = e::class.memberFunctions.firstOrNull { it.name == "withTax" }
    val result = fn?.call(e, 20)

    println(result) // 4.2
}

Yes, it looks like a “string name method call”, and yes, you almost always want to keep it under control. In practice, it’s better to hide such calls behind small wrapper functions and to strictly limit the set of allowed names. Otherwise you’ll quickly build a “mini interpreter” that only you understand.

Safe checks before invocation

When you start invoking methods via reflection, errors become… creative. For example, you passed an argument of the wrong type, or the wrong number of arguments. In static code, the compiler would stop you before you even run it. With reflection, you learn about it at runtime—and often as an exception that looks like “some internal thing”.

So a good style is to add a small “safety layer”: first find the function, then check that it roughly matches by expected number of arguments, then call it, then handle errors carefully.

Let’s build a utility: “call a method with no arguments and return the result string or a clear error text”.

import kotlin.reflect.full.memberFunctions

fun safeCallNoArgs(obj: Any, methodName: String): String {
    val fn = obj::class.memberFunctions.firstOrNull { it.name == methodName }
        ?: return "NO_METHOD($methodName)"

    return try {
        fn.call(obj)?.toString() ?: "null"
    } catch (e: Exception) {
        "CALL_FAILED($methodName): ${e::class.simpleName}"
    }
}

Use it:

fun main() {
    val e = Expense(1, "Coffee", 3.5)

    println(safeCallNoArgs(e, "label"))   // Coffee: 3.5
    println(safeCallNoArgs(e, "oops"))    // NO_METHOD(oops)
}

Notice the output “policy”: we don’t crash, we don’t print a mile-long stack trace; we return a short diagnostic string. For a CLI utility, this is often better than “blowing up the process”.

5. CLI example: the inspect command

Time to use reflection not “in a vacuum”, but as a small feature inside what we already have: a console app that stores expenses in a collection and can execute commands. Let it be the command inspect <id>, which prints the properties of the selected expense. Yes, it’s basically “debugging”, but that’s exactly where reflection often lives.

Let’s make a minimal sketch of a “storage”:

data class Expense(val id: Int, val title: String, val amount: Double)

fun findExpenseById(items: List<Expense>, id: Int): Expense? =
    items.firstOrNull { it.id == id }

Now let’s make an object “dump” via properties. To build a nice multi-line string, it’s convenient to use StringBuilder and its line-appending methods. In Kotlin, many such methods are implemented as extension functions.

import kotlin.reflect.full.memberProperties

fun dumpObject(obj: Any): String {
    val sb = StringBuilder()
    sb.appendLine("type = ${obj::class.simpleName}")

    for (p in obj::class.memberProperties) {
        sb.appendLine("${p.name} = ${p.get(obj)}")
    }
    return sb.toString()
}

And an example usage in a “command-style” flow:

fun main() {
    val expenses = listOf(
        Expense(1, "Coffee", 3.5),
        Expense(2, "Lunch", 12.0)
    )

    val target = findExpenseById(expenses, 2) ?: return
    print(dumpObject(target))
    // type = Expense
    // amount = 12.0
    // id = 2
    // title = Lunch
}

This snippet is easy to plug into your when (command) from previous days: if the command is inspect, parse the id, find the object, print dumpObject(...).

6. A pipeline for safe reflective access

Before we finish, it’s useful to lock one stable pipeline into your head. It is almost always the same: first you find metadata, then validate, then execute, then convert the result into text/value for further logic. It’s like “read → prepare → parse”, but in the world of reflection.

flowchart TD
    A["You have an object obj: Any"] --> B["Need: a property or method by name String"]
    B --> C["Lookup: firstOrNull { it.name == ... }"]
    C -->|not found| D["Fallback: null / NO_METHOD / message"]
    C -->|found| E["Checks: does it match expectations?"]
    E -->|no| F["Fallback: BAD_SIGNATURE / message"]
    E -->|yes| G["Execute: get(obj) / call(obj, args...)"]
    G --> H["try/catch: catch exceptions"]
    H --> I["Normalize result: toString / null / message"]

If you follow this pipeline consistently, reflection stops being “chaos” and becomes a regular utility—just one that needs more careful handling.

7. Common mistakes with reflective access to properties and functions

Mistake #1: forgetting about kotlin-reflect and being surprised that “something is missing”.
The most common scenario: you see kotlin.reflect.full.memberProperties in examples, copy the import, and the IDE highlights it in red. Usually the reason is not that Kotlin “broke”, but that full reflection lives in a separate library. Historically, this even showed up in CLI options where you could disable the reflect jar (-no-reflect). So the first thing to check is dependencies.

Mistake #2: using first { ... } where the element may be absent.
In dynamic string-based lookup, a missing member is a normal situation. If you do first { it.name == "oops" }, you’ll get an exception and a crash on “not found”. firstOrNull + ?: return or ?: "NO_METHOD" makes behavior controlled and readable.

Mistake #3: forgetting about the receiver for get(...) and call(...).
In reflection, you’re not working with “the field value”, but with “the field description”. To turn a description into a value, you need an object. If you try to think “as if it were normal dot access”, it’s easy to lose the receiver and get confused. Keep this formula in mind: “description + object → value”.

Mistake #4: not wrapping reflective invocation in try/catch.
Even if you found everything correctly, the invoked method may itself throw an exception, or you may pass the wrong argument type. In static code, this is often caught earlier. With reflection—later. That’s why reflective calls should almost always be wrapped “narrowly” in try/catch with clear degradation: a message, null, an error result.

Mistake #5: building business logic on string member names.
Reflection is great for diagnostics, debugging, admin commands, and “infrastructure” tasks. But if you start doing critical business logic like “if there is a method "process", call it”, you create a system where correctness depends on strings, not types. Usually it’s better to keep a list of allowed names, or prefer a static contract (an interface/regular call), and leave reflection as a helper tool.

1
Task
Kotlin SELF, level 61, lesson 1
Locked
Property catalog
Property catalog
1
Task
Kotlin SELF, level 61, lesson 1
Locked
Object passport
Object passport
1
Task
Kotlin SELF, level 61, lesson 1
Locked
Access by name
Access by name
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION