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.