skip to content

You must safely cast an erased `Any` (e.g., a deserialized value) into a generic type like `List<User>`. How do you make this robust given type erasure, and when is `@Suppress("UNCHECKED_CAST")` justified?

level: principalimportance: nice to knowfreq 25%

answer

  1. erasure: only List is checked, not User
  2. filterIsInstance<User> = checked per element
  3. type token (KClass/TypeReference/typeOf) avoids the cast
  4. reified removes outer warning, element cast still guards
  5. suppress only with an external invariant + test + comment

basics

~20 s

Because the program forgets the inside type of a list at runtime, you cannot fully trust one big cast. Check each element instead, or use a tool that keeps the type, and only silence the warning when something else guarantees the contents are correct.

solid answer

~40 s

Type erasure means as List<User> only verifies List, so a single cast cannot guarantee elements. Robust options, in order of preference: (1) filterIsInstance<User>() to checked-filter elements; (2) validate each element with as?/is while building the typed list; (3) use a reified inline helper for the outer type and per-element checks; (4) carry a runtime token (KClass<User> or a TypeReference / Jackson TypeFactory) so the deserializer constructs the right type up front. @Suppress("UNCHECKED_CAST") is justified only when an external invariant proves the element type — e.g., a serializer you control wrote it, or a sealed/closed contract guarantees it — and you should localize the suppression to the smallest scope, comment the invariant, and ideally assert it in a test. Otherwise a deferred ClassCastException will surface far from the cast.

code

kotlin · 9 lines
kotlin
// Generic cache where suppression is justified by the insert contract
class TypedCache {
    private val map = mutableMapOf<Class<*>, Any>()
    fun <T : Any> put(type: Class<T>, value: T) { map[type] = value }
    fun <T : Any> get(type: Class<T>): T? {
        @Suppress("UNCHECKED_CAST")  // safe: only put(type, T) ever inserts under `type`
        return map[type] as T?
    }
}

go deeper

for a junior

Understands you can't fully cast to List<User> at runtime and should check elements.

for a middle

Uses filterIsInstance / per-element as? to build a typed list safely.

for a senior

Adds reified helpers and type tokens, and explains the deferred-CCE failure mode.

for a principal

Sets the codebase policy for when suppression is allowed and designs token-based APIs that avoid erased casts entirely, with tests guarding the invariants.

## The constraint: erasure hides element types On the JVM, `List<User>` erases to `List`. A cast `value as List<User>` emits a `checkcast List` and **nothing** for the `User` argument. So the cast can succeed over a list of the wrong elements, and a `ClassCastException` only fires later when an element is implicitly cast to `User`. Any "robust" approach must therefore check **elements**, not just the container. ## Robust strategies (preferred first) **1. `filterIsInstance` — checked and lossless of intent:** ```kotlin fun usersOf(value: Any): List<User> = (value as? List<*>)?.filterIsInstance<User>() ?: emptyList() ``` Every element is `is User`-checked; non-matching elements are dropped rather than crashing later. Use when silently skipping bad elements is acceptable. **2. Validate-and-fail explicitly** when a wrong element should be an error: ```kotlin fun usersStrict(value: Any): List<User> { val raw = value as? List<*> ?: error("not a list") return raw.map { it as? User ?: error("bad element: $it") } } ``` **3. Reified helper** retains the *outer* type for inline call sites: ```kotlin inline fun <reified T> Any.asListOf(): List<T> = (this as? List<*>)?.map { it as T } ?: emptyList() ``` Still relies on per-element `as T`; `reified` removes the outer unchecked warning but element casts remain the real guard. **4. Carry a runtime token** so deserialization builds the correct type and casting is unnecessary: ```kotlin // Jackson example val users: List<User> = mapper.readValue(json, object : TypeReference<List<User>>() {}) ``` A `KClass<T>`, `TypeReference`, or `KType` (from `typeOf<List<User>>()`) preserves the full generic shape at runtime, sidestepping erasure entirely. This is the most robust design when you own the deserializer. ## When `@Suppress("UNCHECKED_CAST")` is justified Suppression is acceptable only when an **invariant outside the type system** proves the element type, such as: - A serializer/format **you control** wrote homogeneous data. - A sealed/closed contract or builder guarantees membership. - A generic cache keyed by `KClass<T>` whose insertion path enforces the type. Discipline when you suppress: - Annotate the **smallest** scope (local val, not the function). - Add a comment stating the invariant that makes it safe. - Back it with a test that would fail if the invariant breaks. ## Design takeaway Prefer designs that never need the cast: reified APIs, type tokens, and per-element validation move the check to where the type information actually exists, turning a deferred, far-away `ClassCastException` into an immediate, local, well-located failure.

  • Why is `filterIsInstance<User>()` safer than `as List<User>`?
    It runs an `is User` check on every element, so the result genuinely contains only Users; the bare cast checks nothing about elements and defers the CCE.
  • What does a type token (e.g., typeOf<List<User>>() / TypeReference) buy you?
    It preserves the full generic type at runtime, letting a deserializer build the correctly typed structure so no erased cast is needed at all.
  • How would you keep a justified suppression honest over time?
    Localize it to the smallest scope, comment the proving invariant, and add a unit test that fails if the invariant (e.g., the only insert path) is ever violated.

saying these in an interview costs you the question

  • Suppressing UNCHECKED_CAST with no stated invariant or test
  • Believing reified makes a List element cast fully checked
  • Casting the whole list instead of validating elements
  • Suppressing at function scope rather than the minimal expression
  • Not recognizing type tokens as the way to defeat erasure cleanly

context