How To Deserialize Generic Types with Moshi?

The moshi-adapters add-on library contains a PolymorphicJsonAdapterFactory class. While the JavaDocs for this library do not seem to be posted, the source does contain a detailed description of its use.

The setup for the example in my question would be:

  private val moshi = Moshi.Builder()
    .add(
      PolymorphicJsonAdapterFactory.of(Vehicle::class.java, "__typename")
        .withSubtype(Car::class.java, "Car")
        .withSubtype(Boat::class.java, "Boat")
    )
    .build()

Now, our Moshi object knows how to convert things like List<Vehicle> to/from JSON, based on the __typename property in the JSON, comparing it to "Car" and "Boat" to create the Car and Boat classes, respectively.


UPDATE 2019-05-25: The newer answer is your best bet. I am leaving my original solution here for historical reasons.


One thing that I had not taken into account is that you can create a type adapter using a generic type, like Map<String, Object>. Given that, you can create a VehicleAdapter that looks up __typename. It will be responsible for completely populating the Car and Boat instances (or, optionally, delegate that to constructors on Car and Boat that take the Map<String, Object> as input). Hence, this is still not quite as convenient as Gson's approach. Plus, you have to have a do-nothing @ToJson method, as otherwise Moshi rejects your type adapter. But, otherwise, it works, as is demonstrated by this JUnit4 test class:

import com.squareup.moshi.FromJson;
import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.Moshi;
import com.squareup.moshi.ToJson;
import com.squareup.moshi.Types;
import org.junit.Assert;
import org.junit.Test;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;

public class Foo {
  static abstract class Vehicle {
    public String id;
    public String name;
  }

  static class Car extends Vehicle {
    public Integer numDoors;
  }

  static class Boat extends Vehicle {
    public String propulsion;
  }

  static class VehicleAdapter {
    @FromJson
    Vehicle fromJson(Map<String, Object> raw) {
      String typename=raw.get("__typename").toString();
      Vehicle result;

      if (typename.equals("Car")) {
        Car car=new Car();

        car.numDoors=((Double)raw.get("numDoors")).intValue();
        result=car;
      }
      else if (typename.equals("Boat")) {
        Boat boat=new Boat();

        boat.propulsion=raw.get("propulsion").toString();
        result=boat;
      }
      else {
        throw new IllegalStateException("Could not identify __typename: "+typename);
      }

      result.id=raw.get("id").toString();
      result.name=raw.get("name").toString();

      return(result);
    }

    @ToJson
    String toJson(Vehicle vehicle) {
      throw new UnsupportedOperationException("Um, why is this required?");
    }
  }

  static final String JSON="[\n"+
    "  {\n"+
    "    \"__typename\": \"Car\",\n"+
    "    \"id\": \"123\",\n"+
    "    \"name\": \"Toyota Prius\",\n"+
    "    \"numDoors\": 4\n"+
    "  },\n"+
    "  {\n"+
    "    \"__typename\": \"Boat\",\n"+
    "    \"id\": \"4567\",\n"+
    "    \"name\": \"U.S.S. Constitution\",\n"+
    "    \"propulsion\": \"SAIL\"\n"+
    "  }\n"+
    "]";

  @Test
  public void deserializeGeneric() throws IOException {
    Moshi moshi=new Moshi.Builder().add(new VehicleAdapter()).build();
    Type payloadType=Types.newParameterizedType(List.class, Vehicle.class);
    JsonAdapter<List<Vehicle>> jsonAdapter=moshi.adapter(payloadType);
    List<Vehicle> result=jsonAdapter.fromJson(JSON);

    assertEquals(2, result.size());

    assertEquals(Car.class, result.get(0).getClass());

    Car car=(Car)result.get(0);

    assertEquals("123", car.id);
    assertEquals("Toyota Prius", car.name);
    assertEquals((long)4, (long)car.numDoors);

    assertEquals(Boat.class, result.get(1).getClass());

    Boat boat=(Boat)result.get(1);

    assertEquals("4567", boat.id);
    assertEquals("U.S.S. Constitution", boat.name);
    assertEquals("SAIL", boat.propulsion);
  }
}

Tags:

Moshi