Unable to serialise list of interfaces using Gson

Consider the following Kotlin code:

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)

[{"recipients":[{"email":"a@b"},{"email":"c@d"}],"name":"A"}]

This is fixed if I change the type of the recipients backing field as follows:

override var recipients: List<RecipientImpl> = listOf()

Does this have something to do with how Kotlin’s interfaces/implementations work versus Java’s? What am I missing here?

Thanks,
Manoj

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?

Hi al3c,

I expect this to be the same as Java,

Here is a sample program that illustrates the different behaviour in serialising in Java.

import static com.google.common.collect.Lists.newArrayList;

import java.util.List;

import com.google.gson.Gson;

public class Experiment {

	public static void main(final String[] args) throws Exception {
		final SubscriptionImpl subscription = new SubscriptionImpl("A");
		subscription.setRecipients(newArrayList(new RecipientImpl("a@b"), new RecipientImpl("c@d")));

		System.out.println(new Gson().toJson(newArrayList(subscription)));

	}

	interface Recipient {

		String getEmail();
	}

	interface Subscription {

		String getName();

		List<Recipient> getRecipients();
	}

	static class RecipientImpl implements Recipient {

		private final String email;

		RecipientImpl(final String email) {
			this.email = email;
		}

		@Override
		public String getEmail() {
			return email;
		}
	}

	static class SubscriptionImpl implements Subscription {

		private final String name;

		private List<Recipient> recipients;

		public SubscriptionImpl(final String name) {
			this.name = name;
		}

		@Override
		public String getName() {
			return name;
		}

		@Override
		public List<Recipient> getRecipients() {
			return recipients;
		}

		public void setRecipients(final List<Recipient> recipients) {
			this.recipients = recipients;
		}
	}
}

The above produced the following output:

> Task :Experiment.main()
[{"name":"A","recipients":[{"email":"a@b"},{"email":"c@d"}]}]

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)

I did some quick debugging and I found out that:

  • 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 :wink:
  • 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:

I was able to get this to work

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

Thanks @dnene

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.

Thanks for the forensics @akurczak! I’ll go through your comment + links in detail and revert.

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