33

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.

clemp6r
  • 3,665
  • 2
  • 26
  • 31

3 Answers3

50

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

@delegate:Transient 
val field by lazy { ... }
Fabian Zeindl
  • 5,860
  • 8
  • 54
  • 78
  • 27
    I (still) get this: `java.lang.NullPointerException: Attempt to invoke interface method 'java.lang.Object kotlin.Lazy.getValue()' on a null object reference`. Using a non-lazy (computed) variable works. – User May 28 '18 at 06:21
  • this answer is not complete, because when I add this annotation, I get the error NullPointerException. This annotation will ignore to serialize this field, but you will have NullPointer when it will try to deserialize the field – Badr Yousfi Apr 11 '23 at 14:59
12

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
Community
  • 1
  • 1
Sergey Mashkov
  • 4,630
  • 1
  • 27
  • 24
  • I copied your code, registered the ExclusionStrategy to Gson, marked the lazy field with the annotation @GsonTransient -> no effect. Did I miss something? – clemp6r Dec 23 '15 at 14:47
  • perhaps you forgot to set it to gson builder – Sergey Mashkov Dec 23 '15 at 14:48
  • Hint: f.getAnnotation is called for the field, but returns null – clemp6r Dec 23 '15 at 14:51
  • 1
    Looking at the bytecode, the `GsonTransient` annotation is attached to a static method `myLazyProp$annotations()` but not to the java field `myLazyProp$delegate` so this cannot work – clemp6r Dec 23 '15 at 15:13
  • 1
    @clemp6r you are right, ok, then we better to always exclude delegate fields, see updated example – Sergey Mashkov Dec 24 '15 at 11:40
  • This makes serialization work but after deserialization the instance is incomplete and accessing the lazy field will throw a `IllegalArgumentException: Parameter specified as non-null is null: method kotlin.LazyKt.getValue, parameter $receiver` – Kirill Rakhman Dec 24 '15 at 11:47
  • @cypressious same as it was before and only if there is no no-arg constructor – Sergey Mashkov Dec 24 '15 at 11:51
  • Ignoring fields finishing by $delegate worked, thanks. – clemp6r Dec 24 '15 at 15:11
  • @cypressious I had the same problem, as a workaround I added default values to each constructor parameter so the Kotlin compiler can create a no-arg constructor. This way Gson calls this no-arg constructor when creating the object, and the lazy field is initialized properly. – clemp6r Dec 24 '15 at 15:16
  • 1
    There is a much simpler way, see my answer: http://stackoverflow.com/a/39205748/342947 – Fabian Zeindl Aug 29 '16 at 12:00
  • When attempting to serialize the lazy values, a casting errror occurs: `java.lang.ClassCastException: java.lang.Class cannot be cast to java.lang.reflect.ParameterizedType`. Are you getting this too? – AliAvci May 09 '18 at 13:40
7

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")
})
jsallaberry
  • 318
  • 2
  • 8