Using Gson, I want to deserialize a Kotlin class that contains a lazy property.
With Kotlin 1.0 beta 4 I get the following error during object deserialization:
Caused by: java.lang.InstantiationException: can't instantiate class kotlin.Lazy
With Kotlin 1.0 beta 2, I used to mark the property with the #Transient annotaiton to tell Gson to skip it. With beta 4 this is not possible anymore, as the annotation causes a compile error.
This annotation is not applicable to target 'member property without backing field'
I can’t figure out how to fix this. Any ideas?
Edit: the lazy property is serialized to JSON ("my_lazy_prop$delegate":{}), but this is not what I want as it is computed from other properties. I suppose if I find a way to prevent the property from being serialized the deserialization crash would be fixed.
Since Kotlin 1.0 simply mark the field like this to ignore it during de/serialization:
#delegate:Transient
val field by lazy { ... }
The reason is that the delegate field is not a backing field actually so it was forbidden. One of the workarounds is to implement ExclusionStrategy: https://stackoverflow.com/a/27986860/1460833
Something like that:
#Retention(AnnotationRetention.RUNTIME)
#Target(AnnotationTarget.FIELD, AnnotationTarget.PROPERTY)
annotation class GsonTransient
object TransientExclusionStrategy : ExclusionStrategy {
override fun shouldSkipClass(type: Class<*>): Boolean = false
override fun shouldSkipField(f: FieldAttributes): Boolean =
f.getAnnotation(GsonTransient::class.java) != null
|| f.name.endsWith("\$delegate")
}
fun gson() = GsonBuilder()
.setExclusionStrategies(TransientExclusionStrategy)
.create()
See related ticket https://youtrack.jetbrains.com/issue/KT-10502
The other workaround is to serialize lazy values as well:
object SDForLazy : JsonSerializer<Lazy<*>>, JsonDeserializer<Lazy<*>> {
override fun serialize(src: Lazy<*>, typeOfSrc: Type, context: JsonSerializationContext): JsonElement =
context.serialize(src.value)
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Lazy<*> =
lazyOf<Any?>(context.deserialize(json, (typeOfT as ParameterizedType).actualTypeArguments[0]))
}
class KotlinNamingPolicy(val delegate: FieldNamingStrategy = FieldNamingPolicy.IDENTITY) : FieldNamingStrategy {
override fun translateName(f: Field): String =
delegate.translateName(f).removeSuffix("\$delegate")
}
Usage example:
data class C(val o: Int) {
val f by lazy { 1 }
}
fun main(args: Array<String>) {
val gson = GsonBuilder()
.registerTypeAdapter(Lazy::class.java, SDForLazy)
.setFieldNamingStrategy(KotlinNamingPolicy())
.create()
val s = gson.toJson(C(0))
println(s)
val c = gson.fromJson(s, C::class.java)
println(c)
println(c.f)
}
that will produce the following output:
{"f":1,"o":0}
C(o=0)
1
As explained by other answers, the delegate field should not be serialized.
You can achieve this with transient in the delegate field, as proposed by #Fabian Zeindl:
#delegate:Transient
val field by lazy { ... }
or skipping all delegate fields in the GsonBuilder, as proposed by #Sergey Mashkov:
GsonBuilder().setExclusionStrategies(object : ExclusionStrategy {
override fun shouldSkipClass(type: Class<*>): Boolean = false
override fun shouldSkipField(f: FieldAttributes): Boolean = f.name.endsWith("\$delegate")
}
However, you may face a NullPointerException if your class doesn't have a no-argument constructor.
It happens because when Gson doesn't find the no-argument constructor, it will use a ObjectConstructor with an UnsafeAllocator using Reflection to construct your object. (see https://stackoverflow.com/a/18645370). This will erase the Kotlin creation of the delegate field.
To fix it, either create a no-argument constructor in your class, or use Gson InstanceCreator to provide Gson with a default object.
GsonBuilder().registerTypeAdapter(YourClass::class, object : InstanceCreator<YourClass> {
override fun createInstance(type: Type?) = YourClass("defaultValue")
})
Related
I am unable to get is_secure object attribute in json response, what is wrong with this code ?
#Configuration
class RouterConfiguration( ) {
#Bean
fun testRoutes(testHandler: TestHandler) = coRouter {
GET("/test", testHandler::testFunction)
}
}
data class TestClass(
val is_secure: Int? = 1,
val xyz: String?
)
#Component
class TestHandler{
suspend fun testFunction(request: ServerRequest): ServerResponse =
ServerResponse.ok().bodyValueAndAwait(TestClass(1,"abc"))
}
is prefixed fields (with camelCase or snake_case pattern) are only serialized if they are of type Boolean. You can find more details about it here.
If you wish to keep the is prefix, you may do so by using #get use-site target. Just use #get:JsonProperty("is_secure") on the is_secure field and it should do.
Thanks reading this question.
this problem confused me.
I created code that response JSON data like below.
#RestController
class JsonTestController {
#GetMapping("jsonTest")
fun jsonTest(): ResponseEntity<HaveBoolean> {
val value = BooleanValue(true)
return ResponseEntity.ok(HaveBoolean(value))
}
data class BooleanValue(val value: Boolean)
data class HaveBoolean(
val isAdmin: BooleanValue,
)
}
and #JsonComponent is below.
#JsonComponent
class BooleanValueJson {
class Serializer : JsonSerializer<JsonTestController.BooleanValue>() {
override fun serialize(value: JsonTestController.BooleanValue, gen: JsonGenerator, serializers: SerializerProvider) {
gen.writeBoolean(value.value)
}
}
class Deserializer : JsonDeserializer<JsonTestController.BooleanValue>() {
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): JsonTestController.BooleanValue =
JsonTestController.BooleanValue(p.valueAsBoolean)
}
}
When I request localhost://8082/jsonTest, I got empty json ({}).
but, I tried other variable name like hoge, mean coding like below.
data class HaveBoolean(
val hoge: BooleanValue,
)
then, I request again, I can get correctly json ({"hoge": true}).
Can't I use isAdmin name on data class ?
Do you have any idea why this problem is happening?
thanks.
This is a known issue with jackson in kotlin. Jackson basically tries to remove is from the name but kotlin data class implementation doesn't have a proper getter without "is" resulting in mismatch. You can add JsonProperty("isAdmin") to the variable and it should work.
data class HaveBoolean(
#get:JsonProperty("isAdmin")
val isAdmin: BooleanValue,
)
I'm trying to create a custom annotation and validator to use in conjunction with the javax validation Api and I'm having trouble access the values of an enum.
The objective of the annotation and the validator is validate if an input data is present within the enum values.
This is the annotation class
import javax.validation.Constraint
import javax.validation.Payload
import kotlin.reflect.KClass
#kotlin.annotation.Target(
AnnotationTarget.FIELD,
)
#kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
#MustBeDocumented
#Constraint(validatedBy = [ValueOfEnumValidator::class])
annotation class ValueOfEnum(
val enumClass: KClass<Enum<*>>,
val message: String ="",
val groups: Array<KClass<*>> = [],
val payload: Array<KClass<out Payload>> = []
)
This is the validator implementation
import javax.validation.ConstraintValidator
import javax.validation.ConstraintValidatorContext
class ValueOfEnumValidator: ConstraintValidator<ValueOfEnum, CharSequence> {
private val acceptedValues: MutableList<String> = mutableListOf()
override fun initialize(constraintAnnotation: ValueOfEnum) {
super.initialize(constraintAnnotation)
acceptedValues.addAll(constraintAnnotation.enumClass.java
.enumConstants
.map {it.name}
)
}
override fun isValid(value: CharSequence?, context: ConstraintValidatorContext): Boolean {
return if (value == null) {
true
} else acceptedValues.contains(value.toString())
}
}
I'm aiming to use annotation like this:
#field:ValueOfEnum(enumClass = SortDirectionEnum::class, message = "{variants.sorted.sort.direction.not.valid}")
var sortDirection:String?=
But my IDE is reporting me the following error in the enumClass parameter
Type mismatch.
Required:KClass<Enum<*>>
Found:KClass<SortDirectionEnum>
How can I make the annotation generic enough to support different enums, and fix this issue ?
You are restricting enumClass to instances of Enum<*>, allowing Enum instances (Enum is an abstract class though, so nothing can be used) with all types of data, you however want to also allow child classes of Enum, which can be achieved with the out keyword there.
val enumClass: KClass<out Enum<*>>,
https://kotlinlang.org/docs/generics.html
Background
I'm developing a Spring Boot application and I'm using Kotlin, IntelliJ and Gradle (Groovy). I have some enum class in my code and I need to persist them (with JPA). I used a simple global converter.
// Sample Enum
enum class Policy {
PUBLIC,
INVITE_ONLY
}
// Sample Converter
#Converter(autoApply = true)
class PolicyConverter : AttributeConverter<Policy, String> {
override fun convertToDatabaseColumn(attribute: Policy): String {
return attribute.name
}
override fun convertToEntityAttribute(dbData: String): Policy {
return Policy.valueOf(dbData.toUpperCase())
}
}
Problem
Since I have 5-6 enums and I hate duplicated code, I thought about a generic converter that should do the work for every given enum. I tried to code something, but nothing worked. This is what I was thinking about:
abstract class EnumConverter<E: Enum<E>> : AttributeConverter<E, String> {
override fun convertToDatabaseColumn(attribute: E): String {
return attribute.name
}
override fun convertToEntityAttribute(dbData: String): E {
return E.valueOf(dbData.toUpperCase())
}
}
In this way I can only extend from one abstract class every enum converter, like so:
#Converter(autoApply = true)
class PolicyConverter : EnumConverter<Policy>() {}
Problem with this code is that I have two errors:
E is red because: Type parameter 'E' cannot have or inherit a companion object, so it cannot be on the left hand side of dot
valueOf is red because: unresolved reference (there are like 150+ types of .valueOf).
As suggested from this I tried to use following function:
private inline fun <reified E : Enum<E>> getValue(string: String): E {
return enumValueOf(string.toUpperCase())
}
But when called from the .convertToEntityAttribute, the result is that "Cannot use 'E' as reified type parameter. Use a class instead."
Question
So the question is simple: how can I implement an easy and fast way to make one converter for all my enums, that all follows the same principle? I just need a return E.valueOf(<value>) function, but it's not present.
A simply workaround of this problem is to define an abstract method that every class will implement and it will return the correct type, given a string.
// Inside EnumConverter, the Generic Class
abstract class EnumConverter<E: Enum<E>> : AttributeConverter<E, String> {
abstract fun getValueFromString(string: String) : E
override fun convertToEntityAttribute(dbData: String): E {
return getValueFromString(dbData)
}
[...]
}
// Inside Policy Enum, implementing
class PolicyConverter : EnumConverter<Policy>() {
override fun getValueFromString(string: String): Policy {
return Policy.valueOf(string.toUpperCase())
}
}
But it's a workaround that I really dislike.
When using Spring's RestTemplate to deserialize some JSON response into an object I fail to do so because I use a Kotlin data class as my object model.
This is the data class:
data class Description (
val descriptionShort: String,
val descriptionLong: String,
val productGroupName: String,
val shortDescriptionProductGroup: String,
val descriptionProductGroupMarketing: String
)
I using these dependencies:
dependencies {
implementation("org.springframework.boot:spring-boot-starter-webflux")
//others
}
dependencyManagement {
imports {
mavenBom("org.springframework.boot:spring-boot-dependencies:2.2.0.RELEASE")
//others
}
dependencies {
dependency("org.springframework.cloud:spring-cloud-stream-reactive:2.2.1.RELEASE")
dependency("com.fasterxml.jackson.module:jackson-module-kotlin:2.10.2")
//others
}
}
The error message when executing unit tests that involves the RestTemplate logic:
Caused by: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of com.company.importer.customer.converter.ut.Description (no Creators, like default construct, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
A friend told me a no-arg constructor is needed.
This is done in Kotlin by giving every property a default value:
data class Description (
val descriptionShort: String = "",
val descriptionLong: String = "",
val productGroupName: String = "",
val shortDescriptionProductGroup: String = "",
val descriptionProductGroupMarketing: String = ""
)
I faced a similar problem many days ago; as first step, I solved the problem as in the response by #xetra11, but I was not very happy with the idea of having default values for technical reasons only.
Finally I solved my issue simply adding the #RequestBody annotation to the controller's method parameter: my method now looks like
fun newTransaction(#RequestBody input: NewTxRequest)
NewTxRequest is defined as follows
data class NewTxRequest(val from: String, val to: String, val amount: BigDecimal)
and the serialization works fine... I hope this can help you, too!