Writing a custom Moshi Adapter
April 18, 2020
Recently I was working on a sample app that demonstrates how to use Kotlin Flow and Channels. The app uses Retrofit, OkHttp and Moshi.
It hits two GitHub API endpoints. One of them required a custom Moshi Codegen Adapter and the other did not. Let’s cover how I implemented both.
Auto-generated Adapter
The first feed is straightforward and does not require a custom Adapter: https://api.github.com/users/<username>/repos
The JSON payload for that endpoint contains a list of repositories where the root element is an Array:
[ { "id": 1, "name": "kotlin" }, { "id": 2, "name": "swift" } ]
The Retrofit service function looks like this:
@GET("users//repos?per_page=$MAX_RESULTS_PER_PAGE") suspend fun getUserRepos( @Path("username") username: String ): List<Repo>
The model object Repo
looks like this:
@JsonClass(generateAdapter = true) data class Repo( val name: String, @Json(name = "stargazers_count") val stars: Int )
My app/build.gradle contains:
// Moshi implementation 'com.squareup.moshi:moshi-kotlin:x.y.z' kapt 'com.squareup.moshi:moshi-kotlin-codegen:x.y.z' // Retrofit implementation 'com.squareup.retrofit2:retrofit:x.y.z' implementation 'com.squareup.retrofit2:converter-moshi:x.y.z' // OkHttp implementation 'com.squareup.okhttp3:okhttp:x.y.z' implementation 'com.squareup.okhttp3:logging-interceptor:x.y.z'
The Dagger module that creates an instance of Retrofit is as follows:
@Provides fun provideRetrofit(): ApiService { val loggingInterceptor = HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY } val okHttpBuilder = OkHttpClient.Builder() .addInterceptor(loggingInterceptor) return Retrofit.Builder() .baseUrl(GITHUB_BASE_URL) .addConverterFactory(MoshiConverterFactory.create()) .client(okHttpBuilder.build()) .build() .create(ApiService::class.java) }
That set up works great for loading a list of Repo
s from an Array of JSON Objects. It does not require any custom adapters since the adapter is auto-generated thanks to Moshi annotation @JsonClass(generateAdapter = true)
A case for custom Codegen Adapter
The other feed https://api.github.com/search/repositories?q=<query> has a different response format. It returns an Array called items
that wraps repository items.
{ "total_count": 2, "items": [ { "id": 1, "name": "kotlin" }, { "id": 2, "name": "swift" } ] }
Here is a function that uses it in the Retrofit interface:
@GET("search/repositories?per_page=$MAX_RESULTS_PER_PAGE") @WrappedRepoList suspend fun getReposForQuery(@Query("q") query: String): List<Repo>
Note, the @WrappedRepoList
annotation to unwrap the repository items so that the return type of the Retrofit function is List<Repo>. This is made possible by a custom Moshi Adapter:
@Retention(AnnotationRetention.RUNTIME) @JsonQualifier annotation class WrappedRepoList @JsonClass(generateAdapter = true) data class RepoList(val items: List<Repo>) class RepoListJsonAdapter { @WrappedRepoList @FromJson fun fromJson(json: RepoList): List<Repo> { return json.items } @ToJson fun toJson(@WrappedRepoList value: List<Repo>): RepoList { throw UnsupportedOperationException() } }
Finally, in order to use this custom Adapter, you need to register it when creating an instance of Retrofit.
@Provides fun provideRetrofit(): ApiService { val loggingInterceptor = HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.HEADERS } val okHttpBuilder = OkHttpClient.Builder() .addInterceptor(loggingInterceptor) val moshi = Moshi.Builder() .add(RepoListJsonAdapter()) .build() return Retrofit.Builder() .baseUrl(GITHUB_BASE_URL) .addConverterFactory(MoshiConverterFactory.create(moshi)) .client(okHttpBuilder.build()) .build() .create(ApiService::class.java) }
Note that .add(KotlinJsonAdapterFactory())
was removed when building moshi
in this case since we are using Codegen, not reflection.
And that’s it. Now the getReposForQuery(q)
unwraps the repository list and returns List<Repo>
using custom Moshi Codegen Adapter.
You can see an example of this code used in an app in this repo.