I have annotation classes
annotation class Endpoint(val name: String)
#Target(AnnotationTarget.TYPE)
annotation class JsonObjectKey(val name: String)
These are used like so
#Endpoint("my-endpoint")
fun myEndpoint(): #JsonObjectKey("key") String {
return "value"
}
With a method: java.lang.reflect.Method object representing myEndpoint,
I am able to access the Endpoint-Annotation, but I fail to access the JsonObjectKey-Annotatation.
method.annotations only contains Endpoint
method.annotatedReturnType.annotations is empty
How to read JsonObjectKey (key in this scenario)?
Intention is to generate JsonObject {"key": "value"}
Use the Kotlin reflection API!
For example, this works:
fun main() {
println(::myEndpoint.returnType.annotations)
}
#Target(AnnotationTarget.TYPE)
annotation class JsonObjectKey(val name: String)
fun myEndpoint(): #JsonObjectKey("key") String {
return "value"
}
This outputs:
[#JsonObjectKey(name=key)]
If you only have a java.lang.reflect.Method for some reason, you can convert it to a KFunction using kotlinFunction:
println(someJavaMethod?.kotlinFunction?.returnType?.annotations)
It appears that annotations annotated in this position in Java:
public static #JsonObjectKey(name = "foo") String foo()
are very different from those annotated in this position in Kotlin:
fun foo(): #JsonObjectKey("key") String = ""
These seem to be two different positions. Kotlin reflection cannot see the annotation on the Java return type, and Java reflection cannot see the annotation on the Kotlin return type.
Compare how the annotations appear in the class file. For the Java method, the annotation shows up in the RuntimeVisibleTypeAnnotations attribute:
RuntimeVisibleTypeAnnotations:
0: #23(#24=s#20): METHOD_RETURN
JsonObjectKey(
name="foo"
)
On the other hand, the Kotlin method instead has the annotation stored as part of the kotlin.Metadata annotation:
RuntimeVisibleAnnotations:
0: #86(#87=[I#88,I#89,I#90],#91=I#92,#93=I#94,#95=[s#96],#97=[s#5,s#98,s#76,s#98,s#99,s#100,s#101,s#102])
kotlin.Metadata(
mv=[1,6,0]
k=2
xi=48
d1=["..."]
d2=["main","","myEndpoint","","LJsonObjectKey;","name","key","myproject"]
)
Related
Using Kotlin serialization, I would like to serialize and deserialize (to JSON) a generic data class with type parameter from a sealed hierarchy. However, I get a runtime exception.
To reproduce the issue:
import kotlinx.serialization.*
import kotlin.test.Test
import kotlin.test.assertEquals
/// The sealed hierarchy used a generic type parameters:
#Serializable
sealed interface Coded {
val description: String
}
#Serializable
#SerialName("CodeOA")
object CodeOA: Coded {
override val description: String = "Code Object OA"
}
#Serializable
#SerialName("CodeOB")
object CodeOB: Coded {
override val description: String = "Code Object OB"
}
/// Simplified class hierarchy
#Serializable
sealed interface NumberedData {
val number: Int
}
#Serializable
#SerialName("CodedData")
data class CodedData<out C : Coded> (
override val number: Int,
val info: String,
val code: C
): NumberedData
internal class GenericSerializerTest {
#Test
fun `polymorphically serialize and deserialize a CodedData instance`() {
val codedData: NumberedData = CodedData(
number = 42,
info = "Some test",
code = CodeOB
)
val codedDataJson = Json.encodeToString(codedData)
val codedDataDeserialized = Json.decodeFromString<NumberedData>(codedDataJson)
assertEquals(codedData, codedDataDeserialized)
}
}
Running the test results in the following runtime exception:
kotlinx.serialization.SerializationException: Class 'CodeOB' is not registered for polymorphic serialization in the scope of 'Coded'.
Mark the base class as 'sealed' or register the serializer explicitly.
This error message does not make sense to me, as both hierarchies are sealed and marked as #Serializable.
I don't understand the root cause of the problem - do I need to explicitly register one of the plugin-generated serializers? Or do I need to roll my own serializer? Why would that be the case?
I am using Kotlin 1.7.20 with kotlinx.serialization 1.4.1
Disclaimer: I do not consider my solution to be very statisfying, but I cannot find a better way for now.
KotlinX serialization documentation about sealed classes states (emphasis mine):
you must ensure that the compile-time type of the serialized object is a polymorphic one, not a concrete one.
In the following example of the doc, we see that serializing child class instead of parent class prevent it to be deserialized using parent (polymorphic) type.
In your case, you have nested polymorphic types, so this is even more complicated I think. To make serialization and deserialization work, then, I've tried multiple things, and finally, the only way I've found to make it work is to:
Remove generic on CodedData (to be sure that code attribute is interpreted in a polymorphic way:
#Serializable
#SerialName("CodedData")
data class CodedData (
override val number: Int,
val info: String,
val code: Coded
): NumberedData
Cast coded data object to NumberedData when encoding, to ensure polymorphism is triggered:
Json.encodeToString<NumberedData>(codedData)
Tested using a little main program based on your own unit test:
fun main() {
val codedData = CodedData(
number = 42,
info = "Some test",
code = CodeOB
)
val json = Json.encodeToString<NumberedData>(codedData)
println(
"""
ENCODED:
--------
$json
""".trimIndent()
)
val decoded = Json.decodeFromString<NumberedData>(json)
println(
"""
DECODED:
--------
$decoded
""".trimIndent()
)
}
It prints:
ENCODED:
--------
{"type":"CodedData","number":42,"info":"Some test","code":{"type":"CodeOB"}}
DECODED:
--------
CodedData(number=42, info=Some test, code=CodeOB(description = Code Object OB))
I have a simple data class being returned from a REST endpoint.
data class SummarizedReturn(
val NET_CASH_FLOW: BigDecimal,
val ROI_PERCENTAGE: BigDecimal
)
When it is returned, the object looks like this:
{
summarizedReturn: {
net_CASH_FLOW: -194703.12028723184,
roi_PERCENTAGE: -35,
}
}
This is not what I need. I need all letters to be capitalized. So I added the JsonProperty annotation
data class SummarizedReturn(
#JsonProperty("NET_CASH_FLOW")
val NET_CASH_FLOW: BigDecimal,
#JsonProperty("ROI_PERCENTAGE")
val ROI_PERCENTAGE: BigDecimal,
)
This did not change anything. I still get the result the same as above.
I then changed the property names and kept the annotation
data class SummarizedReturn(
#JsonProperty("NET_CASH_FLOW")
val netCashFlow: BigDecimal,
#JsonProperty("ROI_PERCENTAGE")
val roiPercentage: BigDecimal,
)
and that returned what I wanted.
{
summarizedReturn: {
NET_CASH_FLOW: -194703.12028723184,
ROI_PERCENTAGE: -35,
}
}
Why did the annotation not work on the initial version of the class? How can I keep my property names all capitalized and have the Jackson value to be the same?
There is an issue with interoperability of Java annotations in Kotlin code. You can register Jackson's Kotlin module to get rid of this problems:
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
fun main() {
val mapper = jacksonObjectMapper() // <= shortcut to ObjectMapper().registerKotlinModule()
println(mapper.writeValueAsString(SummarizedReturn(
BigDecimal("-194703.12028723184"),
BigDecimal("-35"))))
}
Output:
{"NET_CASH_FLOW":-194703.12028723184,"ROI_PERCENTAGE":-35}
This will also require you to add com.fasterxml.jackson.module:jackson-module-kotlin to your dependencies.
PS: Alternatively you can solve it by using slightly different target:
#JsonPropery => #get:JsonPropery (or field:#JsonPropery in case this data class will also be used for deserialization)
A quick demo of a problem:
import kotlin.reflect.jvm.kotlinFunction
interface A<T> {
fun aaa(t: T): String {
return ""
}
}
class B : A<String>
fun main() {
println(B::class.java.methods[0].kotlinFunction) // returns null
}
Calling kotlinFunction on a method without type parameter returns an instance of KFunction as expected.
The reason is type erasure, that occurs in Java, but not in Kotlin:
Java:
public java.lang.String B.aaa(java.lang.Object)
Kotlin:
public java.lang.String B.aaa(java.lang.String)
https://github.com/JetBrains/kotlin/blob/master/core/reflection.jvm/src/kotlin/reflect/jvm/ReflectJvmMapping.kt#L134
Note that it's just Kotlin compiler preserving some more information for reflection, types will be still erased by JVM at runtime, Kotlin or not.
If you need to access Kotlin method, do it directly:
println(B::class.functions.first())
I have a Kotlin annotation:
#Retention(AnnotationRetention.SOURCE)
#Target(AnnotationTarget.CLASS)
annotation class Type(
val type: String
)
It can be used on the Kotlin classes in two ways: using the named parameter syntax, or using the positional parameter syntax:
#Type(type = "named")
data class Named(
…
)
#Type("positional")
data class Positional
…
)
I use this annotation in my custom detekt rules for some extra checks. I need to extract the value of the type parameter to perform some check based on it. I do that like:
private fun getType(klass: KtClass): String? {
val annotation = klass
.annotationEntries
.find {
"Type" == it?.shortName?.asString()
}
val type = (annotation
?.valueArguments
?.find {
it.getArgumentName()?.asName?.asString() == "type"
}
?.getArgumentExpression() as? KtStringTemplateExpression)
?.takeUnless { it.hasInterpolation() }
?.plainContent
return type
}
But this code works only with "named" parameters syntax, and fails for the positional one. Is there any way to get the value of an annotation parameter no matter what syntax was used? It would be perfect if I could acquire my Type annotation instance directly from PSI / AST / KtElements and use it like usually. Is it possible to instantiate an annotation from the PSI tree?
I'm not sure if this is a limitation, a bug or just bad use of GSON. I need to have a hierarchy of Kotlin objects (parent with various subtypes) and I need to deserialize them with GSON. The deserialized object has correct subtype but its field enumField is actually null.
First I thought this is because the field is passed to the "super" constructor but then I found out that "super" works well for string, just enum is broken.
See this example:
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.typeadapters.RuntimeTypeAdapterFactory
open class Parent(val stringField: String,
val enumField: EnumField) {
enum class EnumField {
SUBTYPE1,
SUBTYPE2,
SUBTYPE3
}
}
class Subtype1() : Parent("s1", EnumField.SUBTYPE1)
class Subtype2(stringField: String) : Parent(stringField, EnumField.SUBTYPE2)
class Subtype3(stringField: String, type: EnumField) : Parent(stringField, type)
val subtypeRAF = RuntimeTypeAdapterFactory.of(Parent::class.java, "enumField")
.registerSubtype(Subtype1::class.java, Parent.EnumField.SUBTYPE1.name)
.registerSubtype(Subtype2::class.java, Parent.EnumField.SUBTYPE2.name)
.registerSubtype(Subtype3::class.java, Parent.EnumField.SUBTYPE3.name)
fun main() {
val gson = GsonBuilder()
.registerTypeAdapterFactory(subtypeRAF)
.create()
serializeAndDeserialize(gson, Subtype1()) // this works (but not suitable)
serializeAndDeserialize(gson, Subtype2("s2")) // broken
serializeAndDeserialize(gson, Subtype3("s3", Parent.EnumField.SUBTYPE3)) // broken
}
private fun serializeAndDeserialize(gson: Gson, obj: Parent) {
println("-----------------------------------------")
val json = gson.toJson(obj)
println(json)
val obj = gson.fromJson(json, Parent::class.java)
println("stringField=${obj.stringField}, enumField=${obj.enumField}")
}
Any ideas how to achieve to deserialization of enumField?
(deps: com.google.code.gson:gson:2.8.5, org.danilopianini:gson-extras:0.2.1)
P.S.: Note that I have to use RuntimeAdapterFactory because I have subtypes with different set of fields (I did not do it in the example so it is easier to understand).
Gson requires constructors without arguments to work properly (see deep-dive into Gson code below). Gson constructs raw objects and then use reflection to populate fields with values.
So if you just add some argument-less dummy constructors to your classes that miss them, like this:
class Subtype1() : Parent("s1", EnumField.SUBTYPE1)
class Subtype2(stringField: String) : Parent(stringField, EnumField.SUBTYPE2) {
constructor() : this("")
}
class Subtype3(stringField: String, type: EnumField) : Parent(stringField, type) {
constructor() : this("", EnumField.SUBTYPE3)
}
you will get the expected output:
-----------------------------------------
{"stringField":"s1","enumField":"SUBTYPE1"}
stringField=s1, enumField=SUBTYPE1
-----------------------------------------
{"stringField":"s2","enumField":"SUBTYPE2"}
stringField=s2, enumField=SUBTYPE2
-----------------------------------------
{"stringField":"s3","enumField":"SUBTYPE3"}
stringField=s3, enumField=SUBTYPE3
Gson deep-dive
If you want to investigate the internals of Gson, a tip is to add an init { } block to Subtype1 since it works and then set a breakpoint there. After it is hit you can move up the call stack, step through code, set more breakpoints etc, to reveal the details of how Gson constructs objects.
By using this method, you can find the Gson internal class com.google.gson.internal.ConstructorConstructor and its method newDefaultConstructor(Class<? super T>) that has code like this (I have simplified for brevity):
final Constructor<? super T> constructor = rawType.getDeclaredConstructor(); // rawType is e.g. 'class Subtype3'
Object[] args = null;
return (T) constructor.newInstance(args);
i.e. it tries to construct an object via a constructor without arguments. In your case for Subtype2 and Subtype3, the code will result in a caught exception:
} catch (NoSuchMethodException e) { // java.lang.NoSuchMethodException: Subtype3.<init>()
return null; // set breakpoint here to see
}
i.e. your original code fails since Gson can't find constructors without arguments for Subtype2 and Subtype3.
In simple cases, the problem with missing argument-less constructors is worked around with the newUnsafeAllocator(Type, final Class<? super T>)-method in ConstructorConstructor, but with RuntimeTypeAdapterFactory that does not work correctly.
I may be missing something in what you're trying to achieve, but is it necessary to use the RuntimeTypeAdapterFactory? If we take out the line where we register that in the Gson builder, so that it reads
val gson = GsonBuilder()
.create()
Then the output returns the enum we would expect, which looks to be serialising / deserialising correctly. I.e. the output is:
-----------------------------------------
{"stringField":"s1","enumField":"SUBTYPE1"}
stringField=s1, enumField=SUBTYPE1
-----------------------------------------
{"stringField":"s2","enumField":"SUBTYPE2"}
stringField=s2, enumField=SUBTYPE2
-----------------------------------------
{"stringField":"s3","enumField":"SUBTYPE3"}
stringField=s3, enumField=SUBTYPE3
It also may be an idea to implement Serializable in Parent. i.e.
open class Parent(val stringField: String, val enumField: EnumField) : Serializable {
enum class EnumField {
SUBTYPE1,
SUBTYPE2,
SUBTYPE3
}
}
Try adding #SerializedName annotation to each enum.
enum class EnumField {
#SerializedName("subtype1")
SUBTYPE1,
#SerializedName("subtype2")
SUBTYPE2,
#SerializedName("subtype3")
SUBTYPE3
}