Kotlin is a very concise language, but when its code is compiled into Java bytecode, Kotlin’s elegant constructs disintegrate into Java’s monstrous constructs. At the same time, the use of annotations can play a cruel joke on you.
Let’s consider an example of @JsonProperty annotation and a regular data class. Let’s suppose that we have a data class like this
data class SomeData(@JsonProperty("id") val someDataId: String)
From the code it is logical to assume that the @JsonProperty annotation will be applied to the field someDataId, because we specify it for the field. Thus, our Json file should contain the “id” field instead of “someDataId” and it should work when reading and writing the file.
But is it so? Let’s look into it.
If we decompile Kotlin code, we will see that in Java this class looks like this:
public static final class SomeData { @NotNull // private field private String someDataId; @NotNull // field getter public final String getSomeDataId() { return this.someDataId; } // constructor with field param public SomeData(@NotNull String someDataId) { Intrinsics.checkNotNullParameter(someDataId, "someDataId"); super(); this.someDataId = someDataId; }
As you can see, our someDataId field has been converted into a private field, a getter for the field, and a class constructor parameter. So which of these will the annotation be applied to?
If we look at the definition of the @JsonProperty annotation, we see that it can be applied to a private field, a method and a method parameter. That is, to all Java constructs into which our class data field is decomposed.
@Target({ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) @JacksonAnnotation public @interface JsonProperty
This results in a non-obviousness for the Kotlin compiler. It can apply our annotation to a field, a getter, and a constructor parameter. What do you think it will apply the annotation to in this particular case?
The correct answer is to the constructor parameter of the class.
The real bytecode will look like this and it clearly shows that our annotation applies only to the constructor parameter of the class:
public static final class SomeData { @NotNull private String someDataId; @NotNull public final String getSomeDataId() { return this.someDataId; } public SomeData(@JsonProperty("id") @NotNull String someDataId) { Intrinsics.checkNotNullParameter(someDataId, "someDataId"); super(); this.someDataId = someDataId; }
As a result, our class will be read correctly from Json, but Json generation by our class will not work correctly. When writing to Json, the field name will be “someDataId” instead of the expected “id”.
Let’s look into why Kotlin works this way.
Rules for applying annotations in Kotlin
To resolve such collisions in Kotlin, there is a special rule that says:
If an annotation can be applied to several language constructs at once, it will be applied by default to the first matching use-site from the list:
– Method Parameter
– Property (not available in Java)
– Field
And in order to explicitly resolve such collisions at the code level, Kotlin has a special syntax that allows you to explicitly specify the scope of the annotation.
@[use-site]:Annotation
Examples of specifying the scope of an annotation:
class Example( @field:JsonProperty("Foo") val foo, // annotate Java field @get:JsonProperty("Bar") val bar, // annotate Java getter @param:JsonProperty("Some") val some // annotate Java constructor parameter )
Here is a complete list of use-site types taken from the Kotlin documentation
· file
· property (annotations with this target are not visible to Java)
· field
· get (property getter)
· set (property setter)
· receiver (receiver parameter of an extension function or property)
· param (constructor parameter)
· setparam (property setter parameter)
· delegate (the field storing the delegate instance for a delegated property)
Conclusions
To make our class work properly, we need to specify the annotation like this:
data class SomeData(@field:JsonProperty("id") var someDataId: String)
Or even better, like this to remove the use of reflection to access a private field
data class SomeData( @param:JsonProperty("id") @get:JsonProperty("id") var someDataId: String )
You may never encounter this problem, because usually the scope of annotations is chosen to avoid such collisions. This explains the fact that most Kotlin developers are unaware of this syntax and the ability to explicitly specify the scope of annotations.
But it’s useful to know this so that you don’t look at the solution to your StackOverflow problem as magic, but rather understand how it works and what makes it work.
Study the bytecode that Kotlin generates. It will give you a whole different level of understanding of the language and how your code works.
If you are interested in how Kotlin works under the hood, you can read my other articles about Kotlin.