Annotation Use-Site targets

This is our second entry in our on-going “Advanced Kotlin” blog series. Be sure to check out our first post; Advanced Kotlin - Delegates if you haven’t yet.

In this post we’re going to take a deep-dive into Kotlin Annotation Use-Site targets.

You’ve probably used @get and @set in Kotlin before, but have you come across @receiver or @delegate? Kotlin provides us with nine different Annotation use-site targets. In this post we’ll cover all of them. By writing Kotlin code that uses each of these annotation use-site targets and then decompiling from Kotlin into Java using the Show Kotlin Bytecode -> Decompile tool in IntelliJ we’ll see exactly where each annotation ends up in the resulting code.

The Basics

But first, what exactly are use-site targets and why do we need them?

Many of the libraries, frameworks, and tools we use in Kotlin are actually designed for Java. Either at compile time or at runtime a library may require an annotation to be in a very specific place in your code and/or bytecode for it to function correctly. However, Kotlin is not Java and therefore doesn’t have some of the constructs expected in a Java program, and Kotlin also has some new constructs which are not available to Java. We can use annotation use-site targets to bridge this gap so that various libraries will work as expected.

Simply put, annotation use-site targets allow any @Annotations in your source code to end up at a very specific place in your compiled bytecode or in the Java code generated by kapt.

A Simple Example

Let’s start with a simple example that many of you may have already written yourselves:

@get:SomeAnnotation
var someProperty: String? = null

By prefixing the annotation with @get: the resulting bytecode will have the annotation on the generated getter function:

// Decompiled from the resulting Kotlin Bytecode
@SomeAnnotation
@Nullable
public final String getSomeProperty() {
    return this.someProperty;
}

However, the member variable, the setter function, and any function parameters will not have the SomeAnnotation annotation. In many cases this may be what a framework requires, be it @get:Rule for a JUnit test, @get:ColorRes for an Android resource, or @get:GET for a Retrofit interface.

Overview

In this article we’ll be looking at all nine of the annotation use-site targets available in Kotlin:

  • @get
  • @set
  • @file
  • @param
  • @field
  • @setparam
  • @receiver
  • @property
  • @delegate

Some of these will be either familiar or obvious, such as @get and @set, but some are less obvious and less used, such as @delegate and @receiver. Knowing all of the above is good knowledge to have even if you’ll only use it a handful of times.

We will be using the same method as used above with the @get example in order to test where an annotation resides in decompiled bytecode when using each use-site targets. Each example will begin with an annotation that can be used in our sample code that matches the use-site target name so that it’s easy to spot:

annotation class GetAnnotation
annotation class SetAnnotation
annotation class PropertyAnnotation
annotation class ReceiverAnnotation
...etc...

We’ll then add the matching use-site target to the annotation in some sample Kotlin code, decompile the bytecode and see where the resulting annotation lives.

You may notice that many items on this list closely match Java naming conventions (getters, setters, parameters, etc) as they are intended to help facilitate Kotlin-Java interop. Kotlin’s annotation use-site targets currently only affect JVM bytecode not Kotlin/native, Kotlin/js, or Kotlin Multiplatform projects. This may change in the future, especially for multiplatform language targets such as Swift.

Get and Set use-site targets

Let’s start with the few annotation use-site targets that you’ve probably already seen or used. As an added bonus I’ll also include how you can provide multiple annotations to a single use-site target using [] brackets.

annotation class GetAnnotation
annotation class SetAnnotation
annotation class SetAnnotation2

class Person(
    @get:GetAnnotation val first: String,
    @set:[SetAnnotation SetAnnotation2] var last: String
) {
    // ...
}

The resulting decompiled bytecode is as follows:

public final class Person {
    @NotNull  private final String first;
    @NotNull  private String last;

    @GetAnnotation
    @NotNull
    public final String getFirst() {
        return this.first;
    }

    @NotNull
    public final String getLast() {
        return this.last;
    }

    @SetAnnotation
    @SetAnnotation2
    public final void setLast(@NotNull String var1) {
        this.last = var1;
    }

    public Person(@NotNull String first, @NotNull String last) {
        super();
        this.first = first;
        this.last = last;
    }
}

As you can see, the @GetAnnotation is on the getter and both @SetAnnotation and @SetAnnotation2 are on the setter. They don’t appear anywhere else in the code.

File use-site targets

@file is most commonly used with @file:JvmName in order to provide a custom name for a file. This is useful when a file only contains top-level functions or constants. In this case the Kotlin compiler creates a class called YourFileNameKt, which ends with Kt. This can look odd when used from Java:

@file:JvmName("HttpConstants")
const val HTTP_OK = 200
const val HTTP_NOT_FOUND = 404

Without the above @file:JvmName annotation the usage of these constants from java would appear as HttpConstantsKt.HTTP_OK instead of HttpConstants.HTTP_OK.

The @file use-site target can also be used with other annotations, but since this annotation does not end up in the resulting bytecode it would normally be used by Kotlin-specific tools and libraries, not Java ones. An example of this would be using @file:Suppress to suppress lint or detekt rules for an entire file.

Field and Parameter use-site targets

Let’s look at how @param and @field affect resulting bytecode by using both in a single piece of code on similar class properties:

annotation class ParamAnnotation
annotation class FieldAnnotation

class Person(
    @param:ParamAnnotation val first: String,
    @field:FieldAnnotation val last: String
) {
    // ...
}

The resulting decompiled bytecode is:

public final class Person {
    @NotNull private final String first;

    @FieldAnnotation
    @NotNull
    private final String last;

    @NotNull
    public final String getFirst() {
        return this.first;
    }

    @NotNull
    public final String getLast() {
        return this.last;
    }

    public Person(@ParamAnnotation @NotNull String first, @NotNull String last) {
        super();
        this.first = first;
        this.last = last;
    }
}

As you can see, where @param was used on first, only the parameter to the constructor is annotated. If this was a var instead of a val the parameter to the setter function would not be annotated. We’ll see @setparam shortly which can be used to target setter parameters.

last, being annotated with @field, only has an annotation on the private field used by the getter function. The getter/setter function and the constructor params do not contain the FieldAnnotation annotation.

These can be useful for targeting specific pieces of code when using a dependency injection framework:

class MyClass @Inject constructor(
    @param:SpecificString private val str: String
)

...

@Inject
@field:MainThread
lateinit var scheduler: Scheduler

Setter Parameter use-site targets

As mentioned above, you can use @setparam to add annotations to setter parameters. Let’s compare @set and @setparam in the same example so you can clearly see the difference.

annotation class SetParamAnnotation
annotation class SetAnnotation

class SetParamAnnTest() {
    @setparam:SetParamAnnotation
    @set:SetAnnotation
    var myStr: String? = null
}

Here we’ve added two use-site targets to the same property; @setparam and @set. The resulting decompiled bytecode shows the difference between these 2:

public final class SetParamAnnTest {
    @Nullable
    private String myStr;

    @Nullable
    public final String getMyStr() {
        return this.myStr;
    }

    @SetAnnotation
    public final void setMyStr(@SetParamAnnotation @Nullable String var1) {
        this.myStr = var1;
    }
}

As expected, both the field and getter method are not annotated. As we’ve seen before using @set resulted in the annotation being applied to the setter function. The @setparam use-site target added an annotation to the parameter expected by the setter function.

A combination of these can be useful with some java-based dependency injection libraries:

@set:Inject
@setparam:[Nullable SomeString]
lateinit var str: String

Receiver use-site targets

In Kotlin a “receiver” is the instance on which an extension function is defined, or the type for a lambda with receiver. It is essentially the type on which a block of code is intended to run. In the case of a lambda with receiver in Kotlin, the lambda runs as if it is part of the receiving class. In the case of extension functions, the defined function also runs as if it is part of the receiving class type. The bytecode for extension functions actually contain the receiver as the first parameter to a static function. This allows extension functions to be used by Java code as well as Kotlin code by simply passing the receiving type when used from Java. Knowing this helps us better understand the resulting bytecode and therefore also understand where an @receiver annotation will reside.

annotation class ReceiverAnnotation

fun @receiver:ReceiverAnnotation String.capitalizeVowels() =
    this.map {
        if (it in listOf('a', 'e', 'i', 'o', 'u')) it.toUpperCase() else it
    }.joinToString("")

The result is the function’s receiver parameter being annotated:

public static final String capitalizeVowels(
    @ReceiverAnnotation @Nullable String $receiver
) {
    ...
}

Property use-site targets

@property is the only use-site target that has no effect on the resulting bytecode when viewed from Java. This is due to the fact that Java does not have the same notion of properties as Kotlin. Because of this, adding property-specific annotations will only be useful to kotlin-specific libraries and tools. To demonstrate this we will add two @property-targeted annotations to a class and then use the Kotlin reflection library to see which items are annotated.

annotation class PropertyAnnotation

class SomeClass1(
    @property:PropertyAnnotation val constructorProp: String
) {
    @property:PropertyAnnotation
    val regProp = "Test"
}

As you can see, we’ve added PropertyAnnotation to both a constructor argument and also to a regular Kotlin property.

If we decompile the kotlin bytecode you will see that these annotations are not present in what would be the Java equivalent of this Kotlin code:

public final class SomeClass1 {
    @NotNull
    private final String regProp;

    @NotNull
    private final String constructorProp;

    @NotNull
    public final String getRegProp() {
        return this.regProp;
    }

    @NotNull
    public final String getConstructorProp() {
        return this.constructorProp;
    }

    public SomeClass1(@NotNull String constructorProp) {
        this.constructorProp = constructorProp;
        this.regProp = "Test";
    }
}

In order to read the property annotations we will need to use Kotlin’s reflection library to read the annotations at runtime. This would be similar to how other libraries or tools would need to make use of property-targeted annotations.

fun main(args: Array<String>) {
    for (prop in SomeClass1::class.memberProperties) {
        println("${prop.name} has annotations ${prop.annotations}")
    }
}

The output from this code shows that each member property has the PropertyAnnotation annotation specified:

constructorProp has annotations [@PropertyAnnotation()]
regProp has annotations [@PropertyAnnotation()]

Delegate use-site targets

The previous blog post in our “Advanced Kotlin” series covered delegates in Kotlin. If you are not familiar with delegates I recommend you check out that post.

The @delegate use-site target might be one the hardest to guess where the actual annotation will end up. Is it on the delegate itself? On the delegate class? On the function used to receive the value from the delegate? Let’s take a look at some code and see what happens.

annotation class DelegateAnnotation

class MyDel {
    @delegate:DelegateAnnotation
    val name by lazy { "something" }
}

As you can see from the decompiled code, the annotation will reside on the generated private backing property whose type is the delegate type. The function used to call the delegate (in this case getName) does not receive the annotation.

public final class MyDel {

    @DelegateAnnotation
    @NotNull
    private final Lazy name$delegate;

    @NotNull
    public final String getName() {
        Lazy var1 = this.name$delegate;
        KProperty var3 = $$delegatedProperties[0];
        return (String)var1.getValue();
    }

    public MyDel() {
        this.name$delegate = LazyKt.lazy((Function0)null.INSTANCE);
    }
}

You may find @delegate useful for targeting a property that you wish to wrap in something like a lazy delegate:

@delegate:Transient
val myLock by lazy { ... }

Default Targets

Now that we’ve covered all nine of the available use-site targets, let’s cover what happens when a target is not specified on an annotation.

@SomeAnnotation
val str: String? = null

When an annotation is created, it can itself be annotated with an @Target annotation which lists the targets that are available for the annotation. The available target values for @Target are defined in the AnnotationTarget enum:

  • ‘CLASS’
  • ‘ANNOTATION_CLASS’
  • ‘TYPE_PARAMETER’
  • ‘PROPERTY’
  • ‘FIELD’
  • ‘LOCAL_VARIABLE’
  • ‘VALUE_PARAMETER’
  • ‘CONSTRUCTOR’
  • ‘FUNCTION’
  • ‘PROPERTY_GETTER’
  • ‘PROPERTY_SETTER’
  • ‘TYPE’
  • ‘EXPRESSION’
  • ‘FILE’
  • ‘TYPEALIAS’

The main purpose of these values is to let the compiler know where in source code an annotation is allowed to be used. For example, if an annotation is defined with the CLASS target:

@Target(AnnotationTarget.CLASS)
annotation class ClassAnnotation

Then the annotation can only be applied to a class:

@ClassAnnotation // OK
class Temp {
    @ClassAnnotation // Compilation error!
    val str: String? = null
}

These target values along with the placement of the annotation are used to pick a default use-site target for an annotation if one is not specified. If there’s more than one applicable target either @param (constructor parameters), @property , or @field are used, in that order.

Let’s see an example of this. Using the same example code as above:

@SomeAnnotation
val str: String? = null

If the @SomeAnnotation annotation was defined with @Target(FIELD) then your annotation usage would target using @field as though your code was written as @field:SomeAnnotation. As we saw earlier, using @field means that the private field value would have the annotation in the bytecode. Now, if the @SomeAnnotation annotation in this example had no @Target defined, then it would apply to any element meaning that @property would be chosen first from the list (@param, @property, @field). Remember that @property is only visible from Kotlin so the resulting annotation would not be useful from Java. If you’ve ever struggled with a framework not finding an annotation that you thought you had added to the code, this is the likely culprit.

Stay tuned! More to come!

This article hopefully covered some annotation use-site targets that you have not encountered before. Knowing each of these can often get you out of a bind when trying to use a framework that requires very specific placement of annotations in your code.

If you would like to read more about use-site targets in Kotlin take a look at:

Stay tuned for more!