Suppose we have this JSON:
[
{
"__typename": "Car",
"id": "123",
"name": "Toyota Prius",
"numDoors": 4
},
{
"__typename": "Boat",
"id": "4567",
"name": "U.S.S. Constitution",
"propulsion": "SAIL"
}
]
(there could be many more elements to the list; this just shows two)
I have Car
and Boat
POJOs that use a Vehicle
base class for the common fields:
public abstract class Vehicle {
public final String id;
public final String name;
}
public class Car extends Vehicle {
public final Integer numDoors;
}
public class Boat extends Vehicle {
public final String propulsion;
}
The result of parsing this JSON should be a List<Vehicle>
. The problem is that no JSON parser is going to know, out of the box, that __typename
is how to distinguish a Boat
from a Car
.
With Gson, I can create a JsonDeserializer<Vehicle>
that can examine the __typename
field, identify whether this is a Car
or Boat
, then use deserialize()
on the supplied JsonDeserializationContext
to parse the particular JSON object into the appropriate type. This works fine.
However, the particular thing that I am building ought to support pluggable JSON parsers, and I thought that I would try Moshi as an alternative parser. However, this particular problem is not covered well in the Moshi documentation at the present time, and I am having difficulty figuring out how best to address it.
The closest analogue to JsonDeserializer<T>
is JsonAdapter<T>
. However, fromJson()
gets passed a JsonReader
, which has a destructive API. To find out what the __typename
is, I would have to be able to parse everything by hand from the JsonReader
events. While I could call adapter()
on the Moshi
instance to try to invoke existing Moshi parsing logic once I know the proper concrete type, I will have consumed data off of the JsonReader
and broken its ability to provide the complete object description anymore.
Another analogue of JsonDeserializer<Vehicle>
would be a @FromJson
-annotated method that returns a Vehicle
. However, I cannot identify a simple thing to pass into the method. The only thing that I can think of is to create yet another POJO representing the union of all possible fields:
public class SemiParsedKindOfVehicle {
public final String id;
public final String name;
public final Integer numDoors;
public final String propulsion;
public final String __typename;
}
Then, in theory, if I have @FromJson Vehicle rideLikeTheWind(SemiParsedKindOfVehicle rawVehicle)
on a class that I register as a type adapter with Moshi
, Moshi might be able to parse my JSON objects into SemiParsedKindOfVehicle
instances and call rideLikeTheWind()
. In there, I would look up the __typename
, identify the type, and completely build the Car
or Boat
myself, returning that object.
While doable, this is a fair bit more complex than the Gson approach, and my Car
/Boat
scenario is on the simple end of the possible data structures that I will need to deal with.
Is there another approach to handling this with Moshi that I am missing?
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);
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With