12

假设我们有这个 JSON:

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

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

我有CarBoatPOJO 使用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如何区分 aBoat和 a Car

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

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

最接近的类似物JsonDeserializer<T> JsonAdapter<T>. 但是,fromJson()通过 a JsonReader,它具有破坏性 API。要找出它是什么__typename,我必须能够从JsonReader事件中手动解析所有内容。当我知道正确的具体类型后,我可以调用adapter()实例Moshi尝试调用现有的 Moshi 解析逻辑,但我将消耗来自 的数据JsonReader并破坏其提供完整对象描述的能力。

另一个类似物JsonDeserializer<Vehicle>是一个带@FromJson注释的方法,它返回一个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;
}

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

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

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

4

2 回答 2

20

moshi-adapters附加库包含一个PolymorphicJsonAdapterFactory。虽然这个库的 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 中的属性将诸如 JSON 之类的东西转换为 JSON 之类的东西,并分别将其List<Vehicle>与和创建类。__typename"Car""Boat"CarBoat

于 2019-05-25T23:27:54.813 回答
6

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


我没有考虑到的一件事是您可以使用泛型类型创建类型适配器,例如Map<String, Object>. 鉴于此,您可以创建一个VehicleAdapter查找__typename。它将负责完全填充CarandBoat实例(或者,可选地,将其委托给构造函数,Car并将BoatMap<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);
  }
}
于 2016-12-04T20:35:16.403 回答