In Kotlin, annotations can target multiple elements of a declaration — such as a field, getter, or constructor parameter. When you apply an annotation without explicitly specifying a use-site target (e.g., @MyAnnotation instead of @field:MyAnnotation), Kotlin tries to infer the most appropriate placement.
This default behavior often works well — but in some cases, especially when interoperating with Java frameworks, it can produce unexpected results. Let’s dive into how it works, and what’s changing with Kotlin 2.2.0.
Default Target Inference in Kotlin (Before 2.2.0)
If the annotation supports multiple targets (defined via its @Target declaration), Kotlin infers where to apply the annotation based on context. This is especially relevant for primary constructor properties.
annotation class MyAnnotation
class User(
@MyAnnotation val name: String
)In this case, Kotlin might apply @MyAnnotation to the constructor parameter, property, or field—depending on what @MyAnnotation allows.
Approximate Priority Order:
When multiple targets are applicable, Kotlin historically followed a rough order of priority:
param– Constructor parameterproperty– The Kotlin property itselffield– The backing field generated in bytecode
But this is not a strict rule — the behavior varies by context and Kotlin version.
Interop with Java Frameworks: Why Target Matters
Kotlin properties can generate several elements in Java bytecode:
- A backing field
- A getter method (and setter for
var) - A constructor parameter (for primary constructor properties)
Java frameworks (like Jackson, Spring, Hibernate) often look for annotations in specific places — typically on the field or getter. If Kotlin places the annotation somewhere else (e.g., the property), the framework might not recognize it.
class User(
@JsonProperty("username") val name: String
)If @JsonProperty is placed on the property instead of the field, Jackson may not detect it correctly. The fix is to use an explicit target:
class User(
@field:JsonProperty("username") val name: String
)Kotlin 2.2.0: Refined Defaulting with -Xannotation-default-target
Kotlin 2.2.0 introduces a new experimental compiler flag:
-Xannotation-default-target=param-propertyWhen enabled, this flag enforces a consistent and more predictable defaulting strategy, especially suited for Java interop.
New Priority Order:
param– If valid, apply to the constructor parameterproperty– Ifparamisn’t valid, andpropertyisfield– If neitherparamnorpropertyis valid, butfieldis- Error — If none of these are allowed, compilation fails
This makes annotation behavior more intuitive, especially when integrating with Java-based tools and frameworks.
The @all: Meta-Target (Experimental)
Kotlin 2.2.0 also introduces the experimental @all: use-site target, which applies an annotation to all applicable parts of a property:
param(constructor parameter)property(Kotlin-level property)field(backing field)get(getter)set(setter, ifvar)
Example:
@all:MyAnnotation<br>var name: String = ""This is equivalent to writing:
@param:MyAnnotation
@property:MyAnnotation
@field:MyAnnotation
@get:MyAnnotation
@set:MyAnnotationOnly the targets supported in the annotation’s @Target list will be applied.
Best Practices
Here’s how to work with Kotlin annotations effectively:
| Scenario | Recommendation |
|---|---|
| Using annotations with Java frameworks | Use explicit use-site targets (@field:, @get:) |
| Want consistent defaulting | Enable -Xannotation-default-target=param-property |
| Want broad annotation coverage | Use @all: (if supported by the annotation) |
| Unsure where an annotation is being applied | Use the Kotlin compiler flag -Xemit-jvm-type-annotations and inspect bytecode or decompiled Java |
Conclusion
While Kotlin’s inferred annotation targets are convenient, they don’t always align with Java’s expectations. Starting with Kotlin 2.2.0, you get more control and predictability with:
- Explicit use-site targets
- A refined defaulting flag (
-Xannotation-default-target) - The
@all:meta-target for multi-component coverage
By understanding and controlling annotation placement, you’ll avoid hidden bugs and ensure smooth Kotlin–Java interop.
