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:
- Create the adapter with support for generic arguments
- 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.