Moshi Polymorphic Adapter & Sealed Classes 🔥
How to use Moshi Polymorphic Adapter (+Retrofit) to convert Kotlin sealed classes to/from Json

I was working on some internal app for our team. While I was trying to serialize and deserialize sealed classes, things got a bit messy. Turns out, recent Moshi release has such an easy way around it!
As it was not incredibly easy for me to discover the solution and an example — hope this one could help.
If you haven’t met Moshi already- it’s ok, I’m quite new to it as well. But worry not, it’s pretty easy and really fun!
What was I trying to do?
To get a bit more context:
Nexmo has a powerful Voice API. It allows you, quite easily, to create a series of actions you want to occur during a voice Call. For example: talking (by text to speech), streaming an audio file, collecting the digits pressed of the phone keyboard (input), recording parts of the call, and much more.
You define this series of actions in an object we name NCCO (Nexmo Call Control Object), and send it in a JSON format, when you request Nexmo backend to perform an outgoing voice call.
I created a test app for us, that allows the user to create a series of actions, serialize it to an NCCO in a Json format using Moshi, and sends requests through the Nexmo API with Retrofit, in Kotlin.
Here’s how I did it
0. Start by importing: in your application level build.gradle
, add these libraries :
- The Moshi core library:
implementation "com.squareup.moshi:moshi:1.8.0"
- For converting Kotlin classes to/from Json using reflection. (Another way to work with Kotlin and Moshi is also with codegen)
implementation "com.squareup.moshi:moshi-kotlin:1.8.0"
- To use the PolymorphicJsonAdapterFactory (will elaborate below), we need:
implementation("com.squareup.moshi:moshi-adapters:1.8.0")
- I’ll perform network request with Retrofit, and use the Moshi converter, to convert the request and response body objects to/from JSON:
implementation "com.squareup.retrofit2:retrofit:2.5.0"
implementation "com.squareup.retrofit2:converter-moshi:2.5.0"
The data model
The NCCO, is a list of Actions (Talk, Stream, etc…). The Actions types are differentiated by an “action” field in the JSON. For example, below is an NCCO with 2 Actions: Talk and Stream:
"ncco" :
[
{
"action": "talk",
"text": "You are listening to a Call made with Voice API"
},
{
"action": "stream",
"streamUrl": ["https://acme.com/streams/music.mp3"]
}]
- I created an enum, with the possible actions types:
enum class ActionType {
talk,
record,
stream,
input,
conversation
}
2. the Actions data models: As mentioned, an NCCO has multiple types of actions. This is a classic use case of Kotlin’s sealed classes:
sealed class NccoAction(@Json(name="action") val actionType: ActionType)data class Talk(val text: String, val voiceName: String? = null, val bargeIn: Boolean? = null, val loop: Int? = null : NccoAction(ActionType.talk)data class Record(val eventUrl: Array<String>) : NccoAction(ActionType.record)data class Stream( val streamUrl: List<String>) : NccoAction(ActionType.stream, false)data class Input(val submitOnHash: Boolean = false, val timeOut: Int = 3) : NccoAction(ActionType.input, true)
- Notice: for convenience, in the NccoAction class, I called the field that represents action types- ActionType. To make sure the serilazation will be done with the field name “action” rather than “actionType”, I used the
@Json
annotation :@Json(name=”action”) val actionType
3. Create a Moshi reference:
- Add a
PolymorphicJsonAdapterFactory.
This class is the heart of the magic and helps with the conversation between the sealed class Action to the data classes (Talk, Stream, etc…), and vice versa. It also fits for other abstract classes or interfaces. - You should tell each
PolymorphicJsonAdapterFactory
instance, which sealed class or interface does it in charge of, and by which filed in the Json, the conversation is done. In our case: the NccoAction sealed class, and the “action” field name - Remember to add a Subtype for each class.
var moshi = Moshi.Builder()
.add(
PolymorphicJsonAdapterFactory.of(NccoAction::class.java, "action")
.withSubtype(Talk::class.java, ActionType.talk.name)
.withSubtype(Stream::class.java, ActionType.stream.name)
.withSubtype(Input::class.java, ActionType.input.name)) //if you have more adapters, add them before this line:
.add(KotlinJsonAdapterFactory())
.build()
- Note:
KotlinJsonAdapterFactory
is the adapter for your Kotlin classes. As mentioned, it is used since this example uses reflection. It’s important to make sure to addKotlinJsonAdapterFactory
as the last adapter you add to Moshi builder. During conversion, Moshi will try to use the adapters in the order you added them. Only if it fails, it will try the next adapter.KotlinJsonAdapterFactory
is the most general one, hence should be called last, after all of your custom and special cases were covered.
4. Since I’m using Retrofit for networking, I will add MoshiConverterFactory
to my Retrofit instance, so that Moshi will convert the objects sent and received from the network calls. Don’t forget to create the MoshiConverterFactory
with the Moshi
instance we created before as a parameter.
var retrofit = Retrofit.Builder()
.baseUrl("https://api.nexmo.com/v1/")
.addConverterFactory(MoshiConverterFactory.create(moshi))
.client(httpClient)
//... .build()var nexmoService = retrofit.create<NexmoApiService>(NexmoApiService::class.java)
Basically, that’s it!
Just to complete the picture, here is an example:
5. When creating this object:
val ncco = listOf(
Talk("Hi this is nexmo talking!!"),
Stream(listOf(Tunes.DixieHorn.url)),
Talk("Press 1 if you're happy, press 2 if you're incredibly happy, followed by the hash key"),
Input(timeOut = 5, submitOnHash = true),
Talk("Thank you for the input! Bye Bye")
)
→ it will be serialized to that:
”ncco”:[
{“action”:”talk”,”text”:”Hi this is nexmo talking!!”},
{“action”:”stream” , ”streamUrl”:[“https:.....mp3"]},
{"action":"talk","text":"Press 1 if you’re happy, press 2 if you’re incredibly happy, followed by the hash key”},
{“action”:”input”,”submitOnHash”:true,”timeOut”:5},
{“action”:”talk”,”text”:”Thank you for the input! Bye Bye”}
]
6. I will add it to my request object:
data class PhoneNum(val type: String = "phone", val number: String)val request = NXMRequest(
arrayOf(PhoneNum(number = CALLEE_PHONE_NUM)),
PhoneNum(number = CALLER_PHONE_NUM),
ncco
)
7. when I make the retrofit request:
nexmoService.makeCall(request).enqueue(...)
with this interface:
interface NexmoApiService {
@Headers(
"Authorization: Bearer $APP_JWT",
"Content-Type: application/json"
)
@POST("calls")
fun makeCall(@Body request: NXMRequest): Call<Unit>
}
Then, the phone you requested to call ( set up here on CALLEE_PHONE_NUM) will ring, and the NCCO you created will execute. It’s quite cool, tbh :)
Thank you Zac Sweers for the help! and thank you Jesse Wilson, Eric Cochran, and the team for creating it 👏 🙏