Example: Unwrapping Enveloped JSON
APIs often wrap their responses in a common "envelope" structure, such as a data
object. For example:
{
"data": {
"rank": "4",
"suit": "CLUBS"
}
}
While your networking layer might need to know about the envelope, your application models often only care about the content inside data
. Manually unwrapping this everywhere can be tedious. This example shows how to create a generic JsonAdapter.Factory
to handle this automatically.
The Goal
We want to be able to request an adapter for Card.class
and have it automatically handle the data
envelope, so we can parse the above JSON directly into a Card
object.
The Solution: A Custom Factory and Qualifier
We'll create a @JsonQualifier
annotation to mark when we want to unwrap a response and a JsonAdapter.Factory
to implement the logic.
1. Define the Qualifier
This annotation will signal our factory to apply the unwrapping logic.
@Retention(RUNTIME)
@JsonQualifier
public @interface Enveloped {}
2. Define the Envelope Structure
Create a generic class that represents the envelope structure.
private static final class Envelope<T> {
final T data;
}
3. Create the EnvelopeJsonAdapter.Factory
The factory's create
method will check for our @Enveloped
annotation. If it's present, it will:
- Get an adapter for the
Envelope<T>
type itself. - Create a new
JsonAdapter
that uses the envelope adapter to decode the full structure but only returns the innerdata
field.
public static final class EnvelopeJsonAdapter extends JsonAdapter<Object> {
public static final JsonAdapter.Factory FACTORY = new Factory() {
@Override
public @Nullable JsonAdapter<?> create(Type type, Set<? extends Annotation> annotations, Moshi moshi) {
Set<? extends Annotation> delegateAnnotations = Types.nextAnnotations(annotations, Enveloped.class);
if (delegateAnnotations == null) {
return null; // Not our annotation, move on
}
// Get an adapter for the full Envelope<T> type
Type envelopeType = Types.newParameterizedTypeWithOwner(
EnvelopeJsonAdapter.class, Envelope.class, type);
JsonAdapter<Envelope<?>> delegate = moshi.nextAdapter(this, envelopeType, delegateAnnotations);
return new EnvelopeJsonAdapter(delegate);
}
};
private final JsonAdapter<Envelope<?>> delegate;
EnvelopeJsonAdapter(JsonAdapter<Envelope<?>> delegate) {
this.delegate = delegate;
}
@Override
public Object fromJson(JsonReader reader) throws IOException {
// Decode the envelope, but only return the 'data' part
return delegate.fromJson(reader).data;
}
@Override
public void toJson(JsonWriter writer, Object value) throws IOException {
// Wrap the value in an envelope before serializing
delegate.toJson(writer, new Envelope<>(value));
}
}
4. Usage
First, build a Moshi
instance with the new factory.
Moshi moshi = new Moshi.Builder()
.add(EnvelopeJsonAdapter.FACTORY)
.build();
Now, when you need to parse an enveloped response, request the adapter with the @Enveloped
qualifier.
JsonAdapter<Card> adapter = moshi.adapter(Card.class, Enveloped.class);
Card card = adapter.fromJson("{\"data\":{\"rank\":\"4\",\"suit\":\"CLUBS\"}}");
System.out.println(card); // Output: 4CLUBS
This powerful pattern allows you to cleanly separate API structural concerns (like envelopes) from your core application models.