Reputation: 82007
I've encountered a weird behavior using Gson deserializing within a function with reified
type. It only happens when interfaces
are involved in the type parameter.
Take the following code:
val toBeSerialized = listOf("1337")
with(Gson()) {
val ser = toJson(toBeSerialized)
val deser = fromJson<List<Serializable>>(ser)
}
Line number 4 makes use of a custom extension function Gson.fromJson(json: String): T
.
It fails if T
is defined as reified:
inline fun <reified T> Gson.fromJson(json: String): T = fromJson<T>(json, object : TypeToken<T>() {}.type)
And it works if it is defined as a normal type parameter:
fun <T> Gson.fromJson(json: String): T = fromJson<T>(json, object : TypeToken<T>() {}.type)
(Note that making T
reified makes no sense here, just want to understand its impact in the special use case)
The exception when using reified
looks as follows:
Exception in thread "main" java.lang.RuntimeException: Unable to invoke no-args constructor for ? extends java.io.Serializable. Registering an InstanceCreator with Gson for this type may fix this problem.
at com.google.gson.internal.ConstructorConstructor$14.construct(ConstructorConstructor.java:226)
at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:210)
at com.google.gson.internal.bind.TypeAdapterRuntimeTypeWrapper.read(TypeAdapterRuntimeTypeWrapper.java:41)
at com.google.gson.internal.bind.CollectionTypeAdapterFactory$Adapter.read(CollectionTypeAdapterFactory.java:82)
at com.google.gson.internal.bind.CollectionTypeAdapterFactory$Adapter.read(CollectionTypeAdapterFactory.java:61)
at com.google.gson.Gson.fromJson(Gson.java:888)
at com.google.gson.Gson.fromJson(Gson.java:853)
at com.google.gson.Gson.fromJson(Gson.java:802)
at SoTestsKt.main(SoTests.kt:25)
Caused by: java.lang.UnsupportedOperationException: Interface can't be instantiated! Interface name: java.io.Serializable
at com.google.gson.internal.UnsafeAllocator.assertInstantiable(UnsafeAllocator.java:117)
at com.google.gson.internal.UnsafeAllocator$1.newInstance(UnsafeAllocator.java:49)
at com.google.gson.internal.ConstructorConstructor$14.construct(ConstructorConstructor.java:223)
... 8 more
Upvotes: 2
Views: 2555
Reputation: 6992
As what @Jorn Vernee mentioned, reified
is needed for serializing/deserializing the json. If reified
is not added, it will be treated as List<Object>
and the final deserialized object will have type List<LinkedTreeMap>
and you will get error when you try to use those object as your interface.
To serialize/deserialize interface, you need to register a custom adapter to add the type information of the implemented class. Here is a adapter class where I reference from this site and make some changes on it.
class InterfaceAdapter<T : Serializable> : JsonSerializer<T>, JsonDeserializer<T> {
companion object {
private val CLASSNAME = "CLASSNAME"
private val DATA = "DATA"
}
@Throws(JsonParseException::class)
override fun deserialize(jsonElement: JsonElement, type: Type, jsonDeserializationContext: JsonDeserializationContext): T {
val jsonObject = jsonElement.asJsonObject
val prim = jsonObject.get(CLASSNAME) as JsonPrimitive
val className = prim.asString
val klass = getObjectClass(className)
return jsonDeserializationContext.deserialize(jsonObject.get(DATA), klass)
}
override fun serialize(jsonElement: T, type: Type, jsonSerializationContext: JsonSerializationContext): JsonElement {
val jsonObject = JsonObject()
jsonObject.addProperty(CLASSNAME, jsonElement.javaClass.name)
jsonObject.add(DATA, jsonSerializationContext.serialize(jsonElement))
return jsonObject
}
private fun getObjectClass(className: String): Class<*> {
try {
return Class.forName(className)
} catch (e: ClassNotFoundException) {
throw JsonParseException(e.message)
}
}
}
Register the adapter to the gson object and also provide the type information.
inline fun <reified T> Any.toJson(): String {
val builder = GsonBuilder()
builder.registerTypeAdapter(Serializable::class.java, InterfaceAdapter<Serializable>())
val gson = builder.create()
return gson.toJson(this, object : TypeToken<T>() {}.type)
}
inline fun <reified T> fromJson(json: String): T {
val builder = GsonBuilder()
builder.registerTypeAdapter(Serializable::class.java, InterfaceAdapter<Serializable>())
val gson = builder.create()
return gson.fromJson<T>(json, object : TypeToken<T>() {}.type)
}
Example code:
data class User(val name: String) : Serializable
val data = listOf(User("Sam"))
val ser = data.toJson<List<Serializable>>()
val deser = fromJson<List<Serializable>>(ser)
println(ser)
println(deser.get(0)::class)
println(deser.get(0))
/* Output
[{"CLASSNAME":"User","DATA":{"name":"Sam"}}]
class User
User(name=Sam)
*/
Upvotes: 1
Reputation: 33885
The version that uses reified T
fails because it's trying to de-serialize "1337" as a Serializable
, which is an interface, so it can not be instantiated, and by default there is no type adapter which can de-serialize into a Serializable
(like there is for List<...>
). The easiest way to fix this is to pass List<String>
as a type argument.
In the non-reified version there is no actual type information being passed to your extension function. You can verify this by printing the type you get from the token:
fun <T> Gson.fromJson(json: String): T {
val tt = object : TypeToken<T>() {}.type;
println(tt);
return fromJson(json, tt);
}
In the none reified version that will just print T
(i.e. no actual type information is available), but in the reified
version it will print the actual type (+ any Kotlin declaration site variance modifiers, so List<String>
becomes List<? extends String>
). (I don't know why Gson silently ignores this error of missing type information).
The reason the non reified
version works is a coincidence. Since Gson's default type for de-serializing ["1337"]
is ArrayList<String>
and that is also what you get. It just happens to be assignable to a List
, and since generics are erased there is no class cast exception for the mismatch between String
and Serializable
as the type argument. It works out in the end any ways since String
implements Serializable
.
If you slightly modify the example, where a different cast happens, for instance by specifying a different kind of List
, you run into trouble:
val deser = fromJson<LinkedList<Serializable>>(ser)
Throws a java.lang.ClassCastException: java.util.ArrayList cannot be cast to java.util.LinkedList
You need reified T
to be able to pass on the type information, but it also means that a failure will happen earlier, and does not go unnoticed due to type erasure, like in the non-reified
version.
Upvotes: 5