Custom Adapters with @ToJson and @FromJson
Moshi's default behavior is powerful, but you often need to customize how objects are serialized and deserialized. The easiest way to do this is by creating an adapter object with methods annotated with @ToJson
and @FromJson
.
Simple Value Mapping
Sometimes, you want to represent a complex object as a simple value in JSON, like a string. For example, a Card
object could be represented more compactly than a full JSON object.
Default Representation:
{"rank":"A","suit":"HEARTS"}
Desired Compact Representation:
"AH"
You can create an adapter to handle this transformation:
class CardAdapter {
@ToJson
fun toJson(card: Card): String {
return "" + card.rank + card.suit.name.first()
}
@FromJson
fun fromJson(cardString: String): Card {
if (cardString.length != 2) {
throw JsonDataException("Unknown card: $cardString")
}
val rank = cardString[0]
val suit = when (cardString[1]) {
'C' -> Suit.CLUBS
'D' -> Suit.DIAMONDS
'H' -> Suit.HEARTS
'S' -> Suit.SPADES
else -> throw JsonDataException("Unknown suit: $cardString")
}
return Card(rank, suit)
}
}
To use this adapter, register it with your Moshi.Builder
:
val moshi = Moshi.Builder()
.add(CardAdapter())
.addLast(KotlinJsonAdapterFactory())
.build()
Now, whenever Moshi needs to serialize or deserialize a Card
object, it will use your custom adapter.
Intermediate Object Mapping
Another common scenario is when the JSON structure doesn't align perfectly with your model classes. You can create an adapter that uses an intermediate object to bridge the gap.
Suppose the JSON for an event looks like this:
{
"title": "Blackjack tournament",
"begin_date": "20231027",
"begin_time": "17:00"
}
But your Event
class combines the date and time into a single property:
data class Event(val title: String, val beginDateAndTime: String)
You can create an intermediate class that matches the JSON structure and an adapter to convert between it and your model.
// Intermediate class that matches the JSON
private class EventJson(
val title: String,
val begin_date: String,
val begin_time: String
)
// The adapter to convert between EventJson and Event
class EventAdapter {
@FromJson
fun fromJson(eventJson: EventJson): Event {
return Event(
title = eventJson.title,
beginDateAndTime = "${eventJson.begin_date} ${eventJson.begin_time}"
)
}
@ToJson
fun toJson(event: Event): EventJson {
val parts = event.beginDateAndTime.split(" ")
return EventJson(
title = event.title,
begin_date = parts[0],
begin_time = parts[1]
)
}
}
Register EventAdapter
with Moshi, and it will handle the conversion automatically.
Streaming Adapters
For full control over the serialization and deserialization process, your @ToJson
and @FromJson
methods can accept JsonWriter
and JsonReader
as parameters. This is useful for performance-critical code or complex logic that value mapping can't handle.
class StreamingCardAdapter {
@ToJson
fun toJson(writer: JsonWriter, card: Card) {
writer.value("" + card.rank + card.suit.name.first())
}
@FromJson
fun fromJson(reader: JsonReader): Card {
val cardString = reader.nextString()
// ... parsing logic from above ...
return Card(rank, suit)
}
}
This approach gives you the most power, allowing you to directly manipulate the JSON stream.