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

If you’ve used Moshi for any length of time, you’ve probably come across an error like:

Platform class java.util.ArrayList (with no annotations) requires explicit JsonAdapter to be registered

This means Moshi can’t serialise or deserialise the collection because it’s not just a basic ‘List’. The error message tells you to create a custom JsonAdapter but the documentation doesn’t really tell you how to go about doing this. Let’s fix that with this Blog post.

Creating the adapter will take 2 steps:

  1. Create the adapter with support for generic arguments
  2. Create a factory to create a new adapter for each generic type

So, for example ArrayList<MyObject> and ArrayList<MyOtherObject> will involve creating a new adapter for each type argument. This is because we need to know what type we’re deserialising. Java uses type-erasure for Generics so we don’t know what the generic type is in the brackets. We do know the type in brackets when serialising by checking the javaClass on the item itself, but this would need to be done for every single item which is wasteful. If we save the type up-front, we can just use that type name (and adapter) over and over.

The Generic Adapter

class GenericCollectionAdapter<TItem : Any, TCollection : MutableCollection<TItem>>(
clazz: Class<TItem>,
    moshi: Moshi,
    private val createEmptyCollection: () -> TCollection
) : JsonAdapter<TCollection>() {
    private val typeAdapter = moshi.adapter<TItem>(clazz)

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

        reader.beginArray()

        while (reader.hasNext()) {
            val item = typeAdapter.fromJson(reader)
            if (item != null)
                result.add(item)
        }

        reader.endArray()

        return result
    }

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

        if (value != null) {
            for (item in value) {
                typeAdapter.toJson(writer, item)
            }
        }

        writer.endArray()
    }
}

As you can see in the constructor we require a few parameters, but we can’t pass in Moshi during the build since it’s still being built! To workaround this we can create a factory to instantiate the adapter.

The Generic Factory

class GenericCollectionAdapterFactory<TCollection : MutableCollection<*>>(
    private val collectionClazz: Class<TCollection>,
    private val createEmptyCollection: () -> MutableCollection<Any>
) : JsonAdapter.Factory {
    @Suppress("UNCHECKED_CAST")
    override fun create(type: Type, annotations: MutableSet<out Annotation>, moshi: Moshi): JsonAdapter<*>? {
        val paramType = type as? ParameterizedType ?: return null
        if (paramType.rawType.typeName != collectionClazz.typeName) return null
        if (paramType.actualTypeArguments.size != 1) return null
        val argTypeName = paramType.actualTypeArguments[0].typeName
        val argType = Class.forName(argTypeName) as Class<Any>

        return GenericCollectionAdapter(argType, moshi, createEmptyCollection)
    }
}

By using some creative generic typing, we can get the factory to create adapters for any kind of collection with the type parameter coming from Moshi. So, if you ask for:

val typed = Types.newParameterizedType(ArrayList::class.java, MyObject::class.java)
val adapter = moshi.adapter(typed)

Adding to Moshi

Adding to Moshi couldn’t be easier, you can just add the collection type via the factory:

val moshi = Moshi.Builder()
    .add(GenericCollectionAdapterFactory(ArrayList::class.java) { ArrayList() })
    .build()

This will now allow you to create adapters for specific collection types as long as you’ve registered them up-front with Moshi when building.

But what about times where you’re serialising or deserialising with just the base list type? For example, if you’re using content negotiation with KTor Moshi will ask for an adapter for the ArrayList type, but won’t provide the generic parameter. Fortunately, there is a workaround for this too and that will be the subject of the next blog post.