如何使用 Moshi 反序列化泛型类型?

How To Deserialize Generic Types with Moshi?

假设我们有这个 JSON:

[
  {
    "__typename": "Car",
    "id": "123",
    "name": "Toyota Prius",
    "numDoors": 4
  },
  {
    "__typename": "Boat",
    "id": "4567",
    "name": "U.S.S. Constitution",
    "propulsion": "SAIL"
  }
]

(列表中可能还有更多元素;这里只显示了两个)

我有 CarBoat POJO,它们对公共字段使用 Vehicle 基础 class:

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

解析这个JSON的结果应该是List<Vehicle>。问题是没有 JSON 解析器会开箱即用地知道 __typename 是如何区分 BoatCar

使用 Gson,我可以创建一个 JsonDeserializer<Vehicle> 来检查 __typename 字段,确定这是 Car 还是 Boat,然后使用 deserialize() 在提供的 JsonDeserializationContext 上将特定的 JSON 对象解析为适当的类型。这很好用。

但是,我正在构建的特定东西应该支持可插入 JSON 解析器,我认为我会尝试将 Moshi 作为替代解析器。但是,目前 Moshi 文档中没有很好地涵盖这个特殊问题,我很难找出解决它的最佳方法。

最接近 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 的类比,尝试调用现有的 Moshi 解析逻辑,一旦我知道正确的具体类型,我将消耗 JsonReader 的数据并损坏它能够提供完整的对象描述。

JsonDeserializer<Vehicle> 的另一个类似物是 @FromJson-annotated method,returns 是 Vehicle。但是,我无法确定要传递给该方法的简单内容。我唯一能想到的就是创建另一个代表所有可能字段的联合的 POJO:

public class SemiParsedKindOfVehicle {
  public final String id;
  public final String name;
  public final Integer numDoors;
  public final String propulsion;
  public final String __typename;
}

然后,理论上,如果我在 class 上有 @FromJson Vehicle rideLikeTheWind(SemiParsedKindOfVehicle rawVehicle),我用 Moshi 注册为类型适配器,Moshi 可能能够解析我的 JSON 对象转换为 SemiParsedKindOfVehicle 个实例并调用 rideLikeTheWind()。在那里,我会查找 __typename,确定类型,然后自己完全构建 CarBoat,返回那个对象。

虽然可行,但比 Gson 方法要复杂一些,我的 Car/Boat 场景是我需要处理的可能数据结构的简单端与.

是否有另一种方法可以用我缺少的 Moshi 来处理这个问题?

更新 2019-05-25 是您最好的选择。由于历史原因,我将原来的解决方案留在这里。


有一件事我没有考虑到,您可以使用通用类型创建类型适配器,例如 Map<String, Object>。鉴于此,您可以创建一个查找 __typenameVehicleAdapter。它将负责完全填充 CarBoat 实例(或者,可选地,将其委托给 CarBoat 上的构造函数,将 Map<String, Object> 作为输入)。因此,这仍然不如 Gson 的方法方便。另外,你必须有一个什么都不做的 @ToJson 方法,否则 Moshi 会拒绝你的类型适配器。但是,除此之外,它是有效的,正如这个 JUnit4 测试 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);
  }
}

moshi-adapters add-on 库包含 a PolymorphicJsonAdapterFactory class。虽然这个库的 JavaDocs 似乎没有发布,但源代码确实包含其使用的详细描述。

我问题中示例的设置是:

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

现在,我们的 Moshi 对象知道如何根据 __typename 属性 中的 List<Vehicle> to/from JSON JSON,将其与 "Car""Boat" 进行比较,分别创建 CarBoat 类。