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.
answer
- check property, getter, field, ctor-param in order
- memberProperties for inheritance; define precedence
- cache per (KClass, propName) in ConcurrentHashMap
- javaField null for computed/delegated props
- hot path -> KSP instead of reflection
basics
~20 sLook 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 sBecause @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 linesimport 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
Can read a single property annotation but would miss alternate placements and caching.
Checks property, getter and field but may not handle inheritance, precedence, or caching rigorously.
Builds the multi-element resolver with documented precedence, caching, and edge-case handling.
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.