skip to content

Contrast @property: with @field:. What does each produce at the bytecode/metadata level, and what are the consequences for Java reflection and for annotations declared with only @Target(PROPERTY)?

level: seniorimportance: nice to knowfreq 22%

answer

  1. @field: → real field annotation, Java sees it
  2. @property: → Kotlin metadata only, Java blind
  3. PROPERTY has no JVM element; stored in @kotlin.Metadata
  4. Kotlin reflection reads PROPERTY; java.lang.reflect can't
  5. Default order reaches `property` before `field` — silent interop fail

basics

~10 s

@field: puts the annotation on the real Java field, so Java reflection can read it. @property: stores it in Kotlin-only metadata, so plain Java reflection can't see it — only Kotlin reflection can.

solid answer

~40 s

`@field:` emits a genuine JVM annotation on the backing field; `java.lang.reflect.Field#getAnnotations()` returns it, so Jackson/JPA/etc. see it. `@property:` targets `AnnotationTarget.PROPERTY`, a **Kotlin-only** target: the annotation is recorded in the class's `@kotlin.Metadata` blob, not as a real bytecode annotation on any field or method. Therefore plain Java reflection cannot observe it; you can only read it via **Kotlin reflection** (`kotlin-reflect`, e.g. `KProperty.annotations`). The consequence: an annotation declared with `@Target(AnnotationTarget.PROPERTY)` *only* is invisible to Java-based libraries. To make it reachable from Java, the annotation must also allow (and you must select) `FIELD` or a method target. This is a frequent interop surprise — a custom annotation "works" in Kotlin reflection tests but is ignored by a Java framework at runtime.

code

kotlin · 15 lines
kotlin
import kotlin.reflect.full.memberProperties

@Target(AnnotationTarget.PROPERTY, AnnotationTarget.FIELD)
@Retention(AnnotationRetention.RUNTIME)
annotation class Mark

class C(@property:Mark val a: Int, @field:Mark val b: Int)

fun main() {
    // Kotlin reflection sees both:
    C::class.memberProperties.forEach { println(it.name to it.annotations) }
    // Java reflection: field 'b' has @Mark; field 'a' does NOT.
    println(C::class.java.getDeclaredField("b").annotations.toList())
    println(C::class.java.getDeclaredField("a").annotations.toList()) // empty
}

go deeper

for a junior

Can state @field: is for the field; likely unaware of the metadata distinction.

for a middle

Knows @property: is Kotlin-only and won't reach Java libraries.

for a senior

Explains the @kotlin.Metadata storage, Kotlin-vs-Java reflection visibility, and the default-order interop trap.

for a principal

Audits custom annotation @Target choices across the codebase to guarantee framework visibility.

## What @field: produces `@field:Anno` places `Anno` as a **real JVM annotation on the backing field**. At the bytecode level it appears in the field's `RuntimeVisibleAnnotations` (if the annotation has `RUNTIME` retention). Consequently: - `clazz.getDeclaredField("x").getAnnotations()` returns it. - Java libraries (Jackson, Hibernate, jakarta.validation) that scan fields will find it. - Kotlin reflection also surfaces it. ## What @property: produces `@property:Anno` targets `AnnotationTarget.PROPERTY`. There is **no JVM element called a "property"** — properties are a Kotlin concept. So the compiler stores this annotation inside the class's `@kotlin.Metadata` annotation (the protobuf-encoded blob Kotlin uses to reconstruct declarations). Consequences: - **Java reflection cannot see it.** `Field#getAnnotations()` / `Method#getAnnotations()` won't return it because it isn't attached to any field or method. - **Kotlin reflection can**: with `kotlin-reflect`, `MyClass::x.annotations` (a `KProperty`) exposes it. ```kotlin @Target(AnnotationTarget.PROPERTY) @Retention(AnnotationRetention.RUNTIME) annotation class DocOnly(val note: String) class C(@property:DocOnly("internal") val x: Int) // Kotlin reflection sees it: // C::x.annotations -> [@DocOnly(note=internal)] // Java reflection on the field/getter -> nothing ``` ## The interop trap If you design a custom annotation and give it `@Target(AnnotationTarget.PROPERTY)` only, it will: - pass any test that uses Kotlin reflection, but - be **completely invisible** to a Java framework that scans fields or getters. Fix options: - Add `AnnotationTarget.FIELD` (and/or `FUNCTION`/the getter via method targets) to the annotation's `@Target`, and apply with `@field:`/`@get:`. - Or accept it as a Kotlin-only marker if only Kotlin reflection consumes it. ## Default-order tie-in Recall the default resolution order is **param → property → field**. So an annotation that permits both `PROPERTY` and `FIELD`, placed without a prefix on a body property, resolves to `property` (Kotlin-only, Java-invisible) before it would ever reach `field`. That's exactly why a custom annotation can silently fail Java interop — and why being explicit with `@field:` matters. ## Summary table | | `@field:` | `@property:` | |---|---|---| | Bytecode location | field annotation | inside `@kotlin.Metadata` | | Java reflection | visible | invisible | | Kotlin reflection | visible | visible | | Typical use | JVM-interop libs | Kotlin-only markers |

  • Your custom @Target(PROPERTY) annotation passes Kotlin tests but a Java framework ignores it. Why and how do you fix it?
    PROPERTY lives only in Kotlin metadata, invisible to Java reflection. Add FIELD (or a getter target) to @Target and apply with @field:/@get: so a real bytecode annotation is emitted.
  • Does retention affect whether @property: annotations are readable?
    @property: is always metadata-stored regardless; Kotlin reflection reads it for RUNTIME retention. But it never becomes a Java-visible field/method annotation no matter the retention.

saying these in an interview costs you the question

  • Claiming @property: produces a Java-visible annotation
  • Saying there's a JVM 'property' element
  • Believing Java reflection can read AnnotationTarget.PROPERTY
  • Designing interop annotations with @Target(PROPERTY) only

context