Moshi Adapters for Platform Types like ArrayList and LinkedList (Part 2)

Earlier we created a generic adapter that supported concrete collection types in Moshi. However, sometimes the type of the item within the collection isn’t available. We’ll need to create another adapter but there is a problem – Java type erasure. Even though we might know the type ahead of time in the code, by the time we get to runtime it’s missing. For example, ArrayList<String> will just be ArrayList<*> at runtime – the knowledge we’re dealing with a String is gone. For this reason, we need to add the type information into the JSON. This should only be done if you absolutely can’t specify the types ahead of time using something like:

Type type = Types.newParameterizedType(List.class, Card.class);
JsonAdapter<List<Card>> adapter = moshi.adapter(type);
List<Card> cards = adapter.fromJson(cardsJsonResponse);

The type information will look something like this in the JSON:

[
    {
        "type": "com.mytype",
        "name": "Test"
    },
    {
        "type": "com.mytype",
        "name": "Test2"
    }
]

As you can see type information is added to each object, we can use this to recreate the objects within the collection.

The New Adapter

class UntypedGenericCollectionAdapter<TCollection : MutableCollection<Any>>(
    private val moshi: Moshi,
    private val createEmptyCollection: () -> TCollection
) : JsonAdapter<TCollection>() {

    @FromJson
    override fun fromJson(reader: JsonReader): TCollection? {
        val items = createEmptyCollection()

        reader.beginArray()

        while (reader.hasNext()) {
            val adapter = getAdapter(reader)
            val item = adapter.fromJson(reader)
            if (item != null)
                items.add(item)
        }

        reader.endArray()

        return items
    }

    @ToJson
    override fun toJson(writer: JsonWriter, value: TCollection?) {
        writer.beginArray()

        if (value != null) {
            for (item in value) {
                val adapter = moshi.adapter(item.javaClass)
                adapter.toJson(writer, item)
            }
        }

        writer.endArray()
    }

    private fun getAdapter(reader: JsonReader): JsonAdapter<out Any> {
        val type = getItemType(reader)
            ?: throw JsonDataException("Could not find a type property at ${reader.path}")
        val classEntry = typesSource.allTypes().firstOrNull { it.key == type }
            ?: throw JsonDataException("No adapter found for '$type' at ${reader.path}")
        val classType = classEntry.value
        return moshi.adapter(classType)
    }
}

As you can see, we’re a bit more dynamic in how we create the items. From the type name we can get the class type, once we have that we can pass it directly to Moshi to get the adapter. The adapter will need to output the type property if we want to deserialise the object later.

Next up we need to create a factory like before:

The New Factory

class UntypedGenericCollectionAdapterFactory<TCollection : MutableCollection<*>>(
    private val collectionClazz: Class<TCollection>,
    private val createEmptyCollection: () -> MutableCollection<Any>
) : JsonAdapter.Factory {
    override fun create(type: Type, annotations: MutableSet<out Annotation>, moshi: Moshi): JsonAdapter<*>? {
        if (type !is Class<*>) return null // Don't want to take precedence over the typed generic collections
        if (type.typeName != collectionClazz.typeName) return null

        return UntypedGenericCollectionAdapter(moshi, createEmptyCollection)
    }
}

As before, you can add it to the Moshi builder very simply:

add(UntypedGenericCollectionAdapterFactory(ArrayList::class.java, adapterSource) { ArrayList() })

Now we can handle situations where we don’t know the type within the list whatsoever:

val adapter = moshi.adapter(ArrayList::class.java)
val list = adapter.fromJson(json)