skip to content

Design a robust runtime resolver that, given any Kotlin property, finds a @Serialized(name) annotation regardless of how it was placed, and discuss correctness and performance concerns.

level: seniorimportance: nice to knowfreq 18%

answer

  1. check property, getter, field, ctor-param in order
  2. memberProperties for inheritance; define precedence
  3. cache per (KClass, propName) in ConcurrentHashMap
  4. javaField null for computed/delegated props
  5. hot path -> KSP instead of reflection

basics

~20 s

Look for the annotation in several places for each property: the property itself, its getter, its backing field, and the matching constructor parameter. Take the first one found, and cache results so reflection runs once per class.

solid answer

~30 s

Because @Serialized may use any use-site target, a correct resolver checks multiple elements per property in priority order: property.findAnnotation, property.getter.findAnnotation, property.javaField?.getAnnotation, and the primaryConstructor parameter whose name matches the property. Inheritance means you should use memberProperties (includes supertypes) not just declared ones, and define precedence when both a parent and child annotate. Reflection lookups are expensive, so cache the resolved name per (KClass, propertyName) in a ConcurrentHashMap and make the annotation RUNTIME-retained. Watch for shadowed/overridden members, synthetic properties, and the fact that defaulted parameters are indistinguishable from explicit ones. Guard for missing kotlin-reflect with a clear error.

code

kotlin · 18 lines
kotlin
import kotlin.reflect.full.*
import kotlin.reflect.jvm.javaField

@Retention(AnnotationRetention.RUNTIME) annotation class Serialized(val name: String)

class User(@get:Serialized("user_name") val name: String, val age: Int)

fun main() {
    val k = User::class
    k.memberProperties.forEach { p ->
        val n = p.findAnnotation<Serialized>()?.name
            ?: p.getter.findAnnotation<Serialized>()?.name
            ?: p.javaField?.getAnnotation(Serialized::class.java)?.name
            ?: p.name
        println("${p.name} -> $n")
    }
    // name -> user_name ; age -> age
}

go deeper

for a junior

Can read a single property annotation but would miss alternate placements and caching.

for a middle

Checks property, getter and field but may not handle inheritance, precedence, or caching rigorously.

for a senior

Builds the multi-element resolver with documented precedence, caching, and edge-case handling.

for a principal

Weighs reflection vs KSP, defines precedence/inheritance policy, and engineers the cache for thread-safety and startup cost.

## Why one lookup isn't enough A Kotlin property can carry an annotation on the property element, its getter, its backing field, or — for constructor `val`s — the constructor parameter, depending on the use-site target (`@get:`, `@field:`, `@param:`, or the default). A correct resolver must consult all of them. ## Resolution algorithm ```kotlin import kotlin.reflect.KClass import kotlin.reflect.KProperty1 import kotlin.reflect.full.* import kotlin.reflect.jvm.javaField import java.util.concurrent.ConcurrentHashMap @Retention(AnnotationRetention.RUNTIME) annotation class Serialized(val name: String) private val cache = ConcurrentHashMap<Pair<KClass<*>, String>, String>() fun resolveName(klass: KClass<*>, prop: KProperty1<*, *>): String = cache.getOrPut(klass to prop.name) { val fromProp = prop.findAnnotation<Serialized>()?.name val fromGetter = prop.getter.findAnnotation<Serialized>()?.name val fromField = prop.javaField?.getAnnotation(Serialized::class.java)?.name val fromParam = klass.primaryConstructor ?.parameters?.firstOrNull { it.name == prop.name } ?.findAnnotation<Serialized>()?.name fromProp ?: fromGetter ?: fromField ?: fromParam ?: prop.name } ``` Precedence (property > getter > field > parameter here) must be a documented decision; pick one and stay consistent. ## Correctness concerns - **Inheritance:** use `klass.memberProperties` (includes inherited) if parent classes can annotate; define what happens when both parent and child annotate (override usually wins). - **Overrides/shadowing:** an overriding property does not inherit the parent's annotation unless re-declared. - **Defaults:** you cannot tell a defaulted parameter from an explicitly equal one — don't rely on that distinction. - **Retention:** `@Serialized` must be `RUNTIME`, else every lookup is null. - **Nullability of javaField:** computed/delegated properties have no backing field, so `javaField` is null. ## Performance concerns - Reflection (especially `kotlin-reflect`) is slow relative to direct calls and allocates. **Cache** resolved results keyed by class + property name (here a `ConcurrentHashMap`), so reflection runs once per class, not per serialization call. - Building the member list itself is costly; cache the full property->name map per class rather than re-enumerating. - For truly hot or startup-sensitive paths, prefer compile-time generation (KSP) to eliminate runtime reflection entirely. ## Robustness - Fail fast with a clear message if `kotlin-reflect` is absent (`KotlinReflectionNotSupportedError`). - Treat the resolver as pure given a class, enabling safe caching and easy testing.

  • Why cache the resolution instead of reflecting on every serialize call?
    Reflection is slow and allocates; the mapping is stable per class, so resolving once and caching by class+property avoids repeated overhead.
  • When would javaField be null, breaking a field-only lookup?
    For computed properties (only a getter) or delegated properties, which have no backing field; the resolver must fall through to other elements.

saying these in an interview costs you the question

  • Checking only property.findAnnotation and missing getter/field/param placements.
  • Reflecting on every call with no caching.
  • Ignoring inheritance and override semantics for annotations.
  • Assuming every property has a backing field (javaField non-null).
  • Not handling the missing kotlin-reflect / KotlinReflectionNotSupportedError case.

context