Deserialize Classes with Fields by Delegate Crashes when Using Gson

Gson might collapse in those fields by delegate when deserializing. I want to share what is the root cause and how to resolve this issue.

Bram Yeh
4 min readDec 5, 2019

Introduction

In Kotlin, we want to serialize Topic instance to String and then deserialize after, so we add two functions, toJson() and fromJson(), which were both performed by Gson.

data class Topic (
val title: String? = null,
private val desc: String? = null
) {
companion object {
fun fromJson(string: String?): Topic? {
if (string.isNullOrEmpty()) {
return null
}
return GsonBuilder().create().fromJson(string, Topic::class.java)
}
}
fun toJson(): String {
return GsonBuilder().create().toJson(this)
}
val description: String? by lazy {
desc?.replace("\n", "\n")
}
}

It worked well until one day we added one field with a lazy delegate, val description: String? by lazy, and the fromJson() crashed, and the error log was

E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.bram.demo, PID: 17289
java.lang.RuntimeException: Unable to invoke no-args constructor for interface kotlin.Lazy. Registering an InstanceCreator with Gson for this type may fix this problem.
at com.google.gson.internal.ConstructorConstructor$14.construct(ConstructorConstructor.java:228)
at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:212)
at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$1.read(ReflectiveTypeAdapterFactory.java:131)
at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:222)
at com.google.gson.Gson.fromJson(Gson.java:927)
at com.google.gson.Gson.fromJson(Gson.java:892)
at com.google.gson.Gson.fromJson(Gson.java:841)
at com.google.gson.Gson.fromJson(Gson.java:813)
at com.bram.demo.models.Topic$Companion.fromJson(Topic.kt:53)
at com.bram.demo.models.Topic.fromJson(Unknown Source:2)

It’s a clue why we can’t use lazy with Gson: unable to invoke no-args constructor for interface kotlin.Lazy. The problem lies in the way Gson instantiates classes while deserializing JSON. Gson uses Java’s Unsafe in the UnsafeAllocator.

Lazy Delegate

When we used val description: String? by lazy, Kotlin would combine one private variable in Topic.class, private Lazy<String> description$delegate. The LazyKt.lazy() builder returns a Lazy<T>object, in my case, it is a SynchronizedLazyImpl<T> instance.

However, Gson has no idea which class type description$delegate is during the initialization, even after calling its constructor. It might be SynchronizedLazyImpl, SafePublicationLazyImpl, or UnsafeLazyImpl.

public final class Topic {
@Nullable
public final String title;
@Nullable
public final String desc;
@Nullable
private final Lazy description$delegate;
public Topic(@Nullable String title, @Nullable String desc) {
this.title = title;
this.desc = desc;
this.description$delegate = LazyKt.lazy((new Function0() {
// $FF: synthetic method
// $FF: bridge method
public Object invoke() {
return this.invoke();
}
@Nullable
public final String invoke() {
String var10000 = Topic.this.desc;
return var10000 != null ? StringsKt.replace$default(var10000, "&#92;n", "\n", false, 4, (Object)null) : null;
}
}));

Gson

Gson User Guide said

While deserializing an Object, Gson needs to create a default instance of the class. Well-behaved classes that are meant for serialization and deserialization should have a no-argument constructor.

At the time of deserialization, Gson creates an object with invoking its no-args constructor first. If the class doesn’t have a no-args constructor and we haven’t registered any InstanceCreater objects, then Gson will create an ObjectConstructor with an UnsafeAllocator which uses reflection to create the instance.

UnsafeAllocator calls allocateInstance.invoke(unsafe, c) to simply allocate the memory for an instance without invoking the class constructor, and then uses reflection to initialize the class’ fields if it hasn’t been.

In my case, data class Topic has no-args constructor, Gson will use an ObjectConstructor which uses that default constructor.

However, interface Lazy hasn’t the default no-args constructor, and Gson tries to create it by UnsafeAllocator. The UnsafeAllocator will throw exceptions because kotlin.Lazy is an interface definition, not a concrete class type.

Deserialize and Serialize

When serializing a Topic instance by Gson, the string would look like

{“title”:”disney”,”desc”:”an American entertainment”, ”description$delegate”:{“_value”:{},”initializer”:{}},}

And when deserializing from the above string, Gson collapsed for lack of class type information of description$delegate.

In contrast, when deserializing from the following one, Gson won’t get in trouble. Gson allocates the Topic instance, fills in the title and desc fields, and gently ignore the description$delegate.

{“title”:”disney”,”desc”:”an American entertainment”}

Solution

Now we understand those fields by delegate is the root cause of the crash, and we also observed Gson could deserialize from JSON string, which doesn’t include those fields by delegate. We can resolve this issue by discarding those fields.

If a field is marked transient, (by default) it is ignored and not included in the JSON serialization or deserialization. A transient modifier applied to a field tells Java that this attribute should be excluded when the object is being serialized. When the object is being deserialized, the field will be initialized with its default value (this will typically be a null value for a reference type, or zero/false if the object is a primitive type).

And Gson also supports the transient modifier, we can add @delegate:Transient on those fields by delegate, so that Gson will ignore them during serialization and deserialization.

data class Topic (
val title: String? = null,
private val desc: String? = null
) {
companion object {
fun fromJson(string: String?): Topic? {
if (string.isNullOrEmpty()) {
return null
}
return GsonBuilder().create().fromJson(string, Topic::class.java)
}
}
fun toJson(): String {
return @delegate:Transient(this)
}
@delegate:Transient
val description: String? by lazy {
desc?.replace("&#92;n", "\n")
}
}

With @delegate:Transient, the serialized string wouldn’t contain any description, and this makes Gson bypass delegation at the time of deserialization.

{“title”:”disney”,”desc”:”an American entertainment”}

Here I gave the examples for different cases; class or data class, default value or not, with or without delegate, and @delegate:Transient

GsonKotlinTest

--

--