如何使用 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"
]
(列表中可能还有更多元素;这里只显示了两个)
我有 Car
和 Boat
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
是如何区分 Boat
和 Car
。
使用 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
,识别类型,然后自己完全构建Car
或Boat
,返回该对象。
虽然可行,但它比 Gson 方法复杂得多,而且我的 Car
/Boat
场景是我需要处理的可能数据结构的简单端。
是否有另一种方法来处理我缺少的 Moshi?
【问题讨论】:
【参考方案1】:2019-05-25 更新:newer answer 是您的最佳选择。由于历史原因,我将原来的解决方案留在这里。
我没有考虑到的一件事是您可以使用泛型类型创建类型适配器,例如Map<String, Object>
。鉴于此,您可以创建一个查找__typename
的VehicleAdapter
。它将负责完全填充Car
和Boat
实例(或者,可选择将其委托给Car
和Boat
上的构造函数,它们将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<Vehicle>
等内容转换为 JSON,并将其与 "Car"
和 "Boat"
进行比较以创建 @987654331 @ 和 Boat
类。
【讨论】:
new link以上是关于如何使用 Moshi 反序列化泛型类型?的主要内容,如果未能解决你的问题,请参考以下文章
Kotlin中Json的序列化与反序列化 -- GsonMoshi
Kotlin中Json的序列化与反序列化 -- GsonMoshi