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);
}
}