4
votes

I am using Firestore's Java-based annotation for marking fields and methods for mapping document fields to Java class elements:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.FIELD})
public @interface PropertyName {
  String value();
}

I am using it on a field in a Kotlin data class, which compiles fine:

data class MyDataClass(
    @PropertyName("z") val x: Int
)

In IntelliJ and Android Studio, I can see it show up in the decompiled class dump:

public final data class MyDataClass public constructor(x: kotlin.Int) {
    @field:com.google.cloud.firestore.annotation.PropertyName public final val x: kotlin.Int /* compiled code */

    public final operator fun component1(): kotlin.Int { /* compiled code */ }
}

My impression at this point is that this annotation should be discoverable somehow via Kotlin reflection. As far as I can tell, it is not. I've tried iterating the annotations on:

  1. Each Kotlin data class constructor fields
  2. Each Kotlin field
  3. Each Kotlin function
  4. Each Java constructor
  5. Each Java field
  6. Each Java method

It just does not show up anywhere.

The moment I change the usage of the annotation like this (note the target specifier "get" now):

data class MyDataClass(
    @get:PropertyName("z") val x: Int
)

The annotation now shows up in the generated getter of the Java class object. This is at least workable in practice, but I'm curious why Kotlin lets me compile the annotation in as a field-targeted annotation, but doesn't allow me to get it back out at runtime (unless I'm missing something in the kotlin-reflect APIs?).

If I use this Kotlin-based annotation instead:

@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.PROPERTY)
annotation class PropertyName(val value: String)

With this, the annotation shows up at runtime on the Kotlin field. This is curious because Java's ElementType.FIELD simply does not seem to map perfectly to Kotlin's AnnotationTarget.FIELD.

(Incidentally, if I change this to AnnotationTarget.VALUE_PARAMETER, I can also discover this annotation in the data class constructor parameter.)

This feels like a bug to me, but I'm open to seeing if I just did something wrong here. Or maybe this is just not supported. I'm using Kotlin 1.3.11. Same behavior on JVM and Android.

Code that looks for the annotation:

Log.d("@@@@@", "\n\nDump of $kclass")
val ctor = kclass.constructors.first()
Log.d("@@@@@", "Constructor parameters")
ctor.parameters.forEach { p ->
    Log.d("@@@@@", p.toString())
    Log.d("@@@@@", p.annotations.size.toString())
    p.annotations.forEach { a ->
        Log.d("@@@@@", "  " + a.annotationClass)
    }
}

Log.d("@@@@@", "kotlin functions")
kclass.functions.forEach { f ->
    Log.d("@@@@@", f.toString())
    if (f.annotations.isNotEmpty()) {
        Log.d("@@@@@", "*** " + f.annotations.toString())
    }
}

Log.d("@@@@@", "kotlin members")
kclass.members.forEach { f ->
    Log.d("@@@@@", f.toString())
    if (f.annotations.isNotEmpty()) {
        Log.d("@@@@@", "*** " + f.annotations.toString())
    }
}

Log.d("@@@@@", "kotlin declared functions")
kclass.declaredFunctions.forEach { f ->
    Log.d("@@@@@", f.toString())
    if (f.annotations.isNotEmpty()) {
        Log.d("@@@@@", "*** " + f.annotations.toString())
    }
}

val t = kclass.java
Log.d("@@@@@", "java constructors")
t.constructors.forEach { f ->
    Log.d("@@@@@", f.toString())
}

Log.d("@@@@@", "java methods")
t.methods.forEach { f ->
    Log.d("@@@@@", f.toString())
    if (f.annotations.isNotEmpty()) {
        Log.d("@@@@@", "*** " + f.annotations.toString())
    }
}

Log.d("@@@@@", "java fields")
t.fields.forEach { f ->
    Log.d("@@@@@", f.toString())
    if (f.annotations.isNotEmpty()) {
        Log.d("@@@@@", "*** " + f.annotations.toString())
    }
}
2
It would help to show your Kotlinc code using reflection that fails to work because that is where the error is. You listed statements about it but we don't know if you failed to do those correctly, therefore you could assume there is no bug there, but it might be. Nothing should stop you from seeing that field, it is there, it is clear in the bytecode, but your code isn't showing it. Therefore you have an error. Kotlin does not hide the annotations, including from Java reflection which would work as normal.Jayson Minard
@JaysonMinard Added. There's a lot there.Doug Stevenson
Is the annotation library in the runtime classpath as well?Jayson Minard
Can you show the kclass assignment please, and remove everything about the constructor, it is a field and would not appear there unless it was allowed to be on a method parameter. Plus you checked the bytecode and saw it on the field.Jayson Minard
is it val kclass = MyDataClass::class ? or something else.Jayson Minard

2 Answers

7
votes

The problem here is that my expectations (and possibly the documentation) didn't prepare me for what the Kotlin compiler will do with annotations of various types. My assumption was that a FIELD target annotation target on a Kotlin data class property would apply the annotation directly to the Kotlin synthetic property. This assumption was not true.

What Kotlin will do with a FIELD annotation on a synthetic property is push the FIELD annotation down to the actual backing field for the property in the generated class file. This means that any sort of reflection on the annotated Kotlin property will not find the annotation at all. You have to reach down into the Java Class object to find it.

If you want to annotate a Kotlin class property, and have it found via KClass reflection, you have to use the PROPERTY type annotation, which is unique to Kotlin. With this, if you find the property in the members list of a KClass, it will have that annotation (but not the underlying backing field!).

Going further, with Kotlin data classes, the constructor is the most important thing that defines the properties for the class. So, if you want to create a data class instance via reflection at runtime, it might be best to annotate its properties via its constructor. This means applying an annotation with the VALUE_PARAMETER type to the data class constructor properties, where they can be discovered by reflection of the constructor parameters itself.

In a more general sense, the annotation types that are defined by Java only apply to Java Class reflection, while the annotation types extended by Kotlin only apply to KClass reflection. The Kotlin compiler will forbid you from using Kotlin-specific annotation types on Java elements. The exception here is that, it will allow you to apply Java annotation types to Kotlin concepts (properties with backing fields) that "boil down" to Java native concepts. (FWIW, if you copy Java native annotation code into Kotlin and have it auto-convert, the conversion may not make sense without this in mind.)

If your favorite Java library exposes only annotations that apply to Java layer concepts, consider asking them to provide Kotlin extensions that help you work with their annotations at a more purely Kotlin level. Though this might be tricky to consume in Java code.

Someone please update the docs. :-)

3
votes

While I can find it in Kotlin KClass, I can find it in Java.

@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FIELD)
annotation class PropertyName(val value: String)

data class MyDataClass(
    @PropertyName("z") val x: Int
)

And I use the following code

val a = MyDataClass(1)
a::class.java.declaredFields.forEach {
    it.annotations.forEach { annotation ->
        Log.e(it.name, annotation.toString())
    }
}

It print

2018-12-19 11:33:07.663 25318-25318/com.example.application E/x: @com.example.PropertyName(value=z)