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

Posted

技术标签:

【中文标题】如何使用 Moshi 反序列化泛型类型?【英文标题】:How To Deserialize Generic Types with Moshi? 【发布时间】:2017-04-18 10:17:12 【问题描述】:

假设我们有这个 JSON:

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

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

我有 CarBoat POJO,它们使用 Vehicle 基类作为公共字段:

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,然后在提供的 JsonDeserializationContext 上使用 deserialize() 来解析特定的JSON 对象转换为适当的类型。这很好用。

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

最接近JsonDeserializer<T> is JsonAdapter<T>。然而,fromJson() 被传递了一个 JsonReader,它有一个破坏性的 API。要找出__typename 是什么,我必须能够从JsonReader 事件中手动解析所有内容。虽然我可以在知道正确的具体类型后调用 adapter() on the Moshi instance 来尝试调用现有的 Moshi 解析逻辑,但我将使用来自 JsonReader 的数据并破坏其提供完整对象描述的能力。

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

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

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

虽然可行,但它比 Gson 方法复杂得多,而且我的 Car/Boat 场景是我需要处理的可能数据结构的简单端。

是否有另一种方法来处理我缺少的 Moshi?

【问题讨论】:

【参考方案1】:

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


我没有考虑到的一件事是您可以使用泛型类型创建类型适配器,例如Map<String, Object>。鉴于此,您可以创建一个查找__typenameVehicleAdapter。它将负责完全填充CarBoat 实例(或者,可选择将其委托给CarBoat 上的构造函数,它们将Map<String, Object> 作为输入)。因此,这仍然不如 Gson 的方法方便。另外,你必须有一个无操作的@ToJson 方法,否则 Moshi 会拒绝你的类型适配器。但是,否则,它可以工作,正如这个 JUnit4 测试类所展示的那样:

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 即将发布的 1.4 版本现在更好地支持多态反序列化。 (github.com/square/moshi/issues/89 获取所有详细信息的链接。)感谢您的帖子。我现在会关注 SO 上的 Moshi 标签,所以请解决所有问题! @EricCochran:感谢您提供的信息!但是,除非我误读了问题,否则该问题似乎与多态反序列化无关。 也许最重要的是,JsonAdapter.fromJsonValue(Object) (github.com/square/moshi/pull/234/files) 有助于从地图等中创建这些值。【参考方案2】:

moshi-adapters 附加库包含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 对象知道如何根据 JSON 中的 __typename 属性将 List&lt;Vehicle&gt; 等内容转换为 JSON,并将其与 "Car""Boat" 进行比较以创建 @987654331 @ 和 Boat 类。

【讨论】:

new link

以上是关于如何使用 Moshi 反序列化泛型类型?的主要内容,如果未能解决你的问题,请参考以下文章

Kotlin中Json的序列化与反序列化 -- GsonMoshi

Kotlin中Json的序列化与反序列化 -- GsonMoshi

Kotlin中Json的序列化与反序列化 -- GsonMoshi

无法反序列化泛型类型的集合

Moshi 无法将 0、1 反序列化为“布尔值?”

Gson反序列化泛型类型适配器的基类