clemp6r
clemp6r

Reputation: 3723

Deserializing classes with lazy properties using Gson and Kotlin 1.0 beta 4

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.

Upvotes: 33

Views: 8843

Answers (3)

jsallaberry
jsallaberry

Reputation: 318

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")
})

Upvotes: 7

Fabian Zeindl
Fabian Zeindl

Reputation: 5988

Since Kotlin 1.0 simply mark the field like this to ignore it during de/serialization:

@delegate:Transient 
val field by lazy { ... }

Upvotes: 49

Sergey Mashkov
Sergey Mashkov

Reputation: 4760

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

Upvotes: 13

Related Questions