Example: Handling Multiple JSON Formats for a Single Type

Sometimes an API may provide the same type of data in different JSON structures. For example, a Card could be represented as a compact string ("5D") or as a structured object ({"rank":"5","suit":"DIAMONDS"}).

This example demonstrates how to create a JsonAdapter that can decode both formats but always encodes to the compact string format.

The Challenge

We want to parse both of these JSON snippets into the same Card object:

Format 1: Compact String

"5D"

Format 2: Object

{"rank":"5","suit":"DIAMONDS"}

When we serialize a Card object, we always want to produce the compact string format.

The Solution: A Delegating Adapter

We can solve this by creating a main adapter that checks the type of the incoming JSON token. If it's a string, it delegates to a string-based adapter. If it's an object, it delegates to Moshi's default object adapter.

We'll use a @JsonQualifier to distinguish between our string-only adapter and the default adapter.

1. Define the Qualifier

@Retention(RetentionPolicy.RUNTIME)
@JsonQualifier
@interface CardString {}

2. Create the String-Only Adapter

This adapter is qualified with @CardString and only knows how to handle the compact string format.

public final class CardStringAdapter {
    @ToJson
    String toJson(@CardString Card card) {
        return card.rank + card.suit.name().substring(0, 1);
    }

    @FromJson @CardString
    Card fromJson(String card) {
        // ... logic to parse "5D" into a Card object ...
    }
}

3. Create the Main Delegating Adapter

This adapter is the primary adapter for the Card type. Its fromJson method uses reader.peek() to decide which delegate adapter to use.

public final class MultipleFormatsCardAdapter {
    @ToJson
    void toJson(JsonWriter writer, Card value, @CardString JsonAdapter<Card> stringAdapter)
        throws IOException {
        // Always encode to the string format
        stringAdapter.toJson(writer, value);
    }

    @FromJson
    Card fromJson(
        JsonReader reader,
        @CardString JsonAdapter<Card> stringAdapter, // Our special string adapter
        JsonAdapter<Card> defaultAdapter // Moshi's default object adapter
    ) throws IOException {
        if (reader.peek() == JsonReader.Token.STRING) {
            return stringAdapter.fromJson(reader);
        } else {
            return defaultAdapter.fromJson(reader);
        }
    }
}

4. Register the Adapters

Register all the necessary adapters with Moshi.

Moshi moshi = new Moshi.Builder()
    .add(new MultipleFormatsCardAdapter()) // Main adapter
    .add(new CardStringAdapter())       // String-only adapter
    .build();

JsonAdapter<Card> cardAdapter = moshi.adapter(Card.class);

Now cardAdapter can seamlessly handle both JSON formats for decoding and will consistently produce the compact string format for encoding.