import com.google.gson.Gson
interface Subscription {
val name: String
val recipients: List<Recipient>
}
interface Recipient {
val email: String
}
data class SubscriptionImpl(override val name: String) : Subscription {
// Works if I change field type to `List<RecipientImpl>`
override var recipients: List<Recipient> = listOf()
}
data class RecipientImpl(override val email: String) : Recipient
fun main() {
val subscription = SubscriptionImpl("A").apply {
recipients = listOf(RecipientImpl("a@b"), RecipientImpl("c@d"))
}
println(Gson().toJson(listOf(subscription)))
}
This prints the following result:
[{"recipients":[{},{}],"name":"A"}]
rather than the expected (this result was obtained in Java using similar interfaces and implementations)
I expect this to be the same as Java, Gson uses the declared type to inspect its fields and serialize them.
Here you have a List of some interface type, interfaces have no fields, hence nothing gets serialised.
Also serializing a list of interfaces is possibly confusing; do you expect each element in the Json array to be different if the elements of the original list had different concrete types?
do you expect each element in the Json array to be different if the elements of the original list had different concrete types?
I’m inclined to say yes. The Java paradigm of implementing class deciding the actual behaviour of an interface kinda sorta extends to de/serialised values as well. Just as two concrete classes can have significantly different behaviour, they can have significantly different serialised notations too.
Of course, you can (and should!) write custom de/serialise handlers to handle such cases if they exist in your code. Gson offers hooks in the form of adapters and one of their uses is to handle such cases (however yucky).
(In my specific case there is only one implementation, not several)
for your Java version Gson resolves element type of List<Recipient> to class Recipient, which makes sense, as List interface in Java is invariant.
for Kotlin version Gson resolves element type of List<Recipient> to wildcard type ? extends Recipient. I guess it makes sense, as List is defined in Kotlin as covariant: public interface List<out E>
When serializing collection, Gson gets the actual (runtime) type of element, but only if it was resolved to specific type.
So as a result it for Java version it tries serializing object as RecipentImpl and for Kotlin version it tries to serialize it as ? extends Recipent.
I did not analyzed it deeply enough to know why it’s designed that way
As Gson is field based, it serializes empty Recipent, because fields are in RecipentImpl.
Note, that you could achieve exactly the same behaviour:
Java version would serialize empty Recipents, if you declare the field as List<? extends Recipent>
Kotlin version would serialize fields of Recipent if you declare the property as MutableList<Recipent>, because MutableList is invariant.
I see there are already issues open for Gson related to this behavior:
import com.google.gson.GsonBuilder
import com.google.gson.typeadapters.RuntimeTypeAdapterFactory
interface Subscription {
val name: String
val recipients: List<Recipient>
}
interface Recipient {
val email: String
}
data class SubscriptionImpl(override val name: String) : Subscription {
// Works if I change field type to `List<RecipientImpl>`
override var recipients: List<Recipient> = listOf()
}
data class RecipientImpl(override val email: String) : Recipient
fun main() {
val recipientAdapter = RuntimeTypeAdapterFactory.of(Recipient::class.java, "type")
.registerSubtype(RecipientImpl::class.java, "RecipientImpl")
val subscription = SubscriptionImpl("A").apply {
recipients = listOf(RecipientImpl("a@b"), RecipientImpl("c@d"))
}
println(GsonBuilder().registerTypeAdapterFactory(recipientAdapter).create().toJson(listOf(subscription)))
}
Using a type adapter would certainly do the trick. I was trying to avoid this as I had only one concrete implementation, something that does not necessitate a type adapter in Java.
This should do the trick as well. Since the specific concrete class is now available to the compiler it sidesteps the issues @akurczak pointed to. If the List however contains polymorphic objects which are subtypes of Recipient then the TypeAdapter imo is actually the appropriate way to go since it allows the serialiser to inject the type information into the output stream so that it can then be deserialised correctly when going the opposite direction. (am actually a little miffed at the Java behaviour because roundtrip would mean important type information would be lost and I wonder if that would even be feasible in some circumstances like the one you have where the base type is an interface which doesn’t have a constructor)
import com.google.gson.Gson
interface Subscription<T: Recipient> {
val name: String
val recipients: List<T>
}
interface Recipient {
val email: String
}
data class SubscriptionImpl(override val name: String) : Subscription<RecipientImpl> {
// Works if I change field type to `List<RecipientImpl>`
override var recipients: List<RecipientImpl> = listOf()
}
data class RecipientImpl(override val email: String) : Recipient
fun main() {
val subscription = SubscriptionImpl("A").apply {
recipients = listOf(RecipientImpl("a@b"), RecipientImpl("c@d"))
}
println(Gson().toJson(listOf(subscription)))
}