6

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())
    }
}
7
  • 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. Commented Dec 19, 2018 at 4:36
  • @JaysonMinard Added. There's a lot there. Commented Dec 19, 2018 at 4:37
  • Is the annotation library in the runtime classpath as well? Commented Dec 19, 2018 at 4:37
  • 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. Commented Dec 19, 2018 at 4:39
  • is it val kclass = MyDataClass::class ? or something else. Commented Dec 19, 2018 at 4:40

2 Answers 2

11

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. :-)

Sign up to request clarification or add additional context in comments.

3 Comments

Thanks for the explanation, this just bit me as well. Seeing nothing on KProperty.javaField.annotations was unexpected.
Hmm actually I still don't understand why I'm not seeing anything on KProperty.javaField.annotations, I expected to find the Jackson @JsonProperty annotation there. KProperty.getter.javaMethod.annotations is empty as well.
If I remember correctly, I was looking in the wrong - should have checked the constructor arguments, not the class fields.
5

While I can't 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)

11 Comments

I want to use the original Firestore PropertyName annotation shown at the beginning, not the one I modified in Kotlin for debugging. I don't want to have to introduce a new one. If it's not clear from my question, I don't understand why I can't seem to find the original at all.
@DougStevenson I downloaded com.google.firebase:firebase-database:16.0.5 and it shows 2018-12-19 12:16:16.664 25648-25648/com.example.application E/x: @com.google.firebase.database.PropertyName(value=z) with above code
I have to point out that you're showing a PropertyName annotation that's different than I showed in my question. The Firestore SDK isn't written in Kotlin and it wouldn't have a Kotlin-syntax annotation class. Also, Firestore is different than Realtime Database. The firebase-database artifact is for Realtime Database, not Firestore. Please use what I showed in the question.
@DougStevenson Please state which exact package firebase package are you using. Do you mean com.google.firebase:firebase-admin:6.6.0?
Both that on JVM and firebase-firestore on Android, latest versions of both. They're essentially the same in terms of that annotation, and it hasn't changed over versions. My goal is to automatically map Firestore document fields into the data class. Even if the annotation shows up in the java declared field, I'd prefer to go through a more standard Kotlin understanding of the data class rather than try to depend on the java backing field.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.