Computer scienceProgramming languagesKotlinAdditional instrumentsAdditional code toolsAnnotations

Custom annotations

9 minutes read

You already know about annotations – a way of providing metadata about our code – and how to apply them. Today, we will learn about custom annotations, which is useful for two reasons. First, we'll get a deeper understanding of built-in annotations and of how to use them properly. Second, we will learn to do the magic by ourselves and make our own custom annotations, which you can use, for example, when you create your own library or framework. Now let's get started!

Your first custom annotation

Declaring an annotation is similar to declaring a class – just add the annotation modifier in front of it:

annotation class CustomSuppress

Annotations can also have constructors that take parameters:

annotation class CustomSuppress(vararg val errorName: String)

Notice that vararg means that we can pass zero or more arguments, which is ideal in this situation.

Now let's use our previous custom annotation. Consider the example below:

fun main() {
    @CustomSuppress("UNUSED_VARIABLE")
    val userName = "Alex"

    // This is also correct 
    // because vararg means zero or more arguments
    @CustomSuppress
    val phone = 1234567
}

Yes, it works and there are no errors! Now, if someone writes this code in the IDE, they may wonder why our @CustomSuppress didn't suppress anything. Where is the magic?! Actually, to understand how annotations do their magic, we need to talk about "Annotation processing", which is out of scope of this topic, so now we'll just focus on creating annotations and applying them.

Parameters

What types of parameters are allowed to be passed to an annotation's constructor?

According to the official documentation, the allowed parameter types include:

  • Types that correspond to Java primitive types (Int, Long, etc.)

  • Strings

  • Classes (Foo::class)

  • Enums

  • Other annotations

  • Arrays of the types listed above

The question now is: what is not allowed? Again quoting the official documentation, "Annotation parameters cannot have nullable types because the JVM does not support storing null as a value of an annotation attribute". The code in the following example will give an error:

// Compile time error
// nullable types are NOT allowed
annotation class CustomSuppress(vararg val errorName: String?)

Last but not least, if you pass an annotation as a parameter of another annotation, don't prefix it with the @ character and do it like in the example below:

// A new custom annotation
annotation class Special(val why: String)

// Passing Special annotation as parameter
annotation class CustomSuppress(val name: Special)

We have created two custom annotations and passed the Special annotation as a parameter to the second one – CustomSuppress. Simply put, think about passing it as a parameter like in the case of normal class instantiation.

Now, let's use CustomSuppress from the example above and apply it:

fun main() {
    @CustomSuppress(name = Special("IDK"))
    val userName = "Alexandr"
}

To avoid confusion, applying an annotation is completely different from passing it as a parameter: to apply an annotation, you need to use the @ symbol. So let's edit the previous example:

// A new Custom annotation
annotation class Special(val why: String)

// Passing Special annotation as parameter
// Applying @Special to this parameter
annotation class CustomSuppress(@Special("IDK") val name: Special)

The last thing to be mentioned in this section is that an annotation parameter cannot be 'var':

//Compile time error
annotation class CustomSuppress(var name: String)

Meta-annotations

Annotations provide metadata about our code, right? However, we can also use annotations to provide metadata about other annotations. The annotations that annotate other annotations are called "Meta-annotations".

Meta-annotations are the following:

1. @Target: as the name implies, this annotation indicates which kinds of elements can be targeted. Consider the example below:

@Target(AnnotationTarget.LOCAL_VARIABLE)
annotation class Special

AnnotationTarget is an enum that contains a list of possible targets; you can find the full list in the documentation.

The @Special annotation is applicable only to local variables. Consider the example below:

fun main() {
    // applicable to local variables
    @Special val name = "Ali"

    // Error as this annotation 
    // is not applicable to target 'expression'
    println(@Special name)
}

You can also target as many elements as you want:

@Target(
    AnnotationTarget.LOCAL_VARIABLE,
    AnnotationTarget.FIELD,
    AnnotationTarget.FUNCTION,
    AnnotationTarget.CLASS
)
annotation class Special

If the @Target meta-annotation is not present in the annotation declaration, the annotation is applicable to the following elements by default:

  • CLASS

  • PROPERTY

  • FIELD

  • LOCAL_VARIABLE

  • VALUE_PARAMETER

  • CONSTRUCTOR

  • FUNCTION

  • PROPERTY_GETTER

  • PROPERTY_SETTER.

The @Target meta-annotation is very useful when you need to make your custom annotation work only with specific elements: it guarantees that you always get the expected results.

2. @Retention: this meta-annotation determines whether the annotation will be stored in binary output or the compiler will get rid of it when the program runs.

There are only three available options in the enum AnnotationRetention that we can choose from:

SOURCE

Annotation isn't stored in binary output.

BINARY

Annotation is stored in binary output but is invisible for reflection.

RUNTIME

Annotation is stored in binary output and is visible for reflection (default retention).

Let's take an example:

@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.SOURCE)
annotation class VeryCool

We have created an annotation called VeryCool, chosen functions as the only target, and finally specified that the annotation's retention will be SOURCE, which means that the annotation won't be stored in binary output and will be deleted when the program runs.

You may ask: why do we create an annotation that will be deleted later? Sometimes it's useful when we need the annotation to work only at the compile time, like in the case with the @Suppress annotation.

3. Sometimes you want to apply an annotation to the same element multiple times, and here @Repeatable comes to the rescue:

fun main() {
    @Special
    @Special
    @Special
    val name = "Ali"
}

@Repeatable
annotation class Special

4. @MustBeDocumented specifies that the annotation must be part of the element's generated documentation.

Conclusion

In this topic, we have learned how to make our own custom annotations and how to specify their parameters. We've also learned different meta-annotations like @Target, @MustBeDocumented, @Repeatable, and @Retention.

Annotating is easy, even if it might be a bit tricky the first time. Now, let's enforce our understanding with some practice.

26 learners liked this piece of theory. 4 didn't like it. What about you?
Report a typo