skip to content

How do Kotlin's retention defaults and AnnotationRetention map to Java's @Retention/RetentionPolicy, and what interop gotcha follows from the differing defaults?

level: seniorimportance: nice to knowfreq 20%

answer

  1. BINARY (Kotlin) = CLASS (Java)
  2. Kotlin default RUNTIME, Java default CLASS
  3. Kotlin annotation w/o @Retention IS reflective; Java's is NOT
  4. AnnotationTarget ≈ ElementType, plus property/getter/setter
  5. Declare retention explicitly across the boundary

basics

~10 s

Kotlin's SOURCE/BINARY/RUNTIME line up with Java's SOURCE/CLASS/RUNTIME. The catch: Kotlin defaults to RUNTIME while Java defaults to CLASS (BINARY), so a Kotlin annotation with no @Retention is reflection-visible but a Java one isn't.

solid answer

~40 s

kotlin.annotation.AnnotationRetention maps onto java.lang.annotation.RetentionPolicy as SOURCE→SOURCE, BINARY→CLASS, RUNTIME→RUNTIME. When you compile a Kotlin annotation, the compiler emits the corresponding Java @Retention into the bytecode so Java reflection sees it consistently. The key gotcha is the differing default: Kotlin defaults to RUNTIME, but Java's @Retention default is CLASS (the equivalent of Kotlin BINARY). So a Kotlin annotation with no @Retention is readable by both Kotlin and Java reflection, whereas a Java annotation with no @Retention is invisible to reflection. Likewise Kotlin AnnotationTarget maps to Java ElementType, but Kotlin adds property/getter/setter-style targets that have no direct Java field equivalent. When sharing annotations across the language boundary, declare retention explicitly to avoid surprises.

code

kotlin · 6 lines
kotlin
// Kotlin: no @Retention => RUNTIME => visible to reflection
annotation class KAuto

// Equivalent explicit Java would need:
//   @Retention(RetentionPolicy.RUNTIME) @interface JAuto {}
// because Java's default (CLASS) would hide it from reflection.

go deeper

for a junior

May know the three levels but not the cross-language default mismatch.

for a middle

Knows BINARY=CLASS and that defaults differ, enough to debug a missing annotation.

for a senior

Articulates the full mapping and proactively declares retention explicitly across the boundary; knows the property-target gap.

for a principal

Establishes team conventions (always explicit retention, documented targets) to keep mixed Kotlin/Java annotation contracts robust.

## Retention mapping Kotlin's `kotlin.annotation.AnnotationRetention` and Java's `java.lang.annotation.RetentionPolicy` correspond one-to-one: | Kotlin AnnotationRetention | Java RetentionPolicy | |---|---| | SOURCE | SOURCE | | BINARY | CLASS | | RUNTIME | RUNTIME | Note the naming mismatch: Kotlin's **BINARY** is Java's **CLASS** (both mean "in the class file, not retained for reflection"). When the Kotlin compiler compiles an annotation class, it writes the matching Java `@Retention(RetentionPolicy.X)` into the bytecode, so Java reflection and Kotlin reflection agree on visibility. ## The defaults gotcha - **Kotlin default retention = RUNTIME.** - **Java default retention = CLASS** (i.e., BINARY). So: ```kotlin annotation class KMarker // no @Retention → RUNTIME → reflection-visible ``` ```java @interface JMarker {} // no @Retention → CLASS → NOT reflection-visible ``` A developer porting a Java annotation to Kotlin (or vice versa) who relies on "the default" gets opposite reflection behavior. If a Kotlin reflective framework can't find a Java-declared annotation, a missing `@Retention(RetentionPolicy.RUNTIME)` on the Java side is a prime suspect. ## Target mapping Kotlin `AnnotationTarget` maps onto Java `ElementType` for the shared kinds: CLASS↔TYPE, FUNCTION↔METHOD, VALUE_PARAMETER↔PARAMETER, FIELD↔FIELD, CONSTRUCTOR↔CONSTRUCTOR, LOCAL_VARIABLE↔LOCAL_VARIABLE, ANNOTATION_CLASS↔ANNOTATION_TYPE, TYPE_PARAMETER↔TYPE_PARAMETER, TYPE↔TYPE_USE, FILE↔PACKAGE-ish. But Kotlin adds **PROPERTY, PROPERTY_GETTER, PROPERTY_SETTER** which have no single Java counterpart — Java sees only the generated methods/fields. This is why use-site targets exist and why a Kotlin property annotation may not appear where Java reflection looks. ## Practical guidance - When an annotation is shared across the boundary, **always declare @Retention explicitly** rather than trusting defaults. - For runtime frameworks consuming Java annotations from Kotlin, confirm the Java side used `RetentionPolicy.RUNTIME`. - Remember the BINARY/CLASS naming difference when reading mixed codebases.

  • A Kotlin DI framework can't see a Java-declared annotation at runtime. What's the first thing to check?
    Whether the Java annotation has @Retention(RetentionPolicy.RUNTIME); Java defaults to CLASS, which is invisible to reflection.
  • Which Java RetentionPolicy does Kotlin's BINARY correspond to?
    CLASS — same meaning, different name.

Same three shelves, but Kotlin stocks the top (visible) shelf by default while Java stocks the middle (hidden) shelf by default.

saying these in an interview costs you the question

  • Assuming Java and Kotlin share the same default retention
  • Thinking BINARY and CLASS are different semantics rather than just different names
  • Expecting Kotlin's PROPERTY/getter/setter targets to map cleanly to Java
  • Relying on defaults for cross-language annotations

context