提问人:Aleksandr Kravets 提问时间:8/10/2020 更新时间:8/17/2020 访问量:2256
将多态对象列表反序列化到“对象”字段中
Deserialize List of polymorphic objects into Object field
问:
我的 REST 端点返回包含 Object 字段的 Response 对象。序列化一切正常,但是当我开始为此 API 编写客户端时,我遇到了反序列化问题。我根据一些关于 Jackson 多态序列化的问题/文章制作了这个示例代码。它演示了这个问题。
@Data
abstract class Animal {
String name;
}
@Data
class Dog extends Animal {
boolean canBark;
}
@Data
class Cat extends Animal {
boolean canMeow;
}
@Data
public class Zoo {
private Object animals;
}
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "name", visible = true)
@JsonSubTypes({
@JsonSubTypes.Type(name = "dog", value = Dog.class),
@JsonSubTypes.Type(name = "cat", value = Cat.class)
})
public class Mixin {
}
public class Main {
private static final String JSON_STRING = "{\n"
+ " \"animals\": [\n"
+ " {\"name\": \"dog\"},\n"
+ " {\"name\": \"cat\"}\n"
+ " ]\n"
+ "}";
public static void main(String[] args) throws IOException {
ObjectMapper objectMapper = Jackson.newObjectMapper()
.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE)
.setDefaultPropertyInclusion(Include.NON_NULL);
objectMapper.addMixIn(Animal.class, Mixin.class);
Zoo zoo = objectMapper.readValue(JSON_STRING, Zoo.class);
for (Object animal : (Collection) zoo.getAnimals()) {
System.out.println(animal.getClass());
}
}
}
我期望的(以及我所拥有的 Zoo#animals 类型)在输出中:List<Animals>
class jackson.poly.Dog
class jackson.poly.Cat
我现在对 Object 有什么:
class java.util.LinkedHashMap
class java.util.LinkedHashMap
但是除了动物列表之外,我还需要反序列化其他类型的对象。请帮忙!
答:
如果您知道要转换为的类型,则可以使用 的方法。convertValue
ObjectMapper
您可以转换单个值或整个列表。
请看以下示例:
public static void main(String[] args) throws IOException {
ObjectMapper objectMapper = Jackson.newObjectMapper()
.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE)
.setDefaultPropertyInclusion(Include.NON_NULL);
objectMapper.addMixIn(Animal.class, Mixin.class);
Zoo zoo = objectMapper.readValue(JSON_STRING, Zoo.class);
List<Animal> animals = objectMapper.convertValue(zoo.getAnimals(), new TypeReference<List<Animal>>(){});
for (Object animal : animals) {
System.out.println(animal.getClass());
}
}
对于您的评论,如果您不确定需要处理的实际类型,一个有用的选项是启用 Jackson 的默认类型(并采取必要的预防措施)。
你可以这样做:
PolymorphicTypeValidator validator = BasicPolymorphicTypeValidator
.builder()
.allowIfBaseType(Animal.class)
.build()
;
ObjectMapper mapper = new ObjectMapper();
mapper.activateDefaultTyping(DefaultTyping.NON_FINAL, validator);
您可以做的另一种想法是为包含任意信息的字段定义自定义。Deserializer
您可以重用一些代码,而不是从头开始创建一个代码。
当您定义为您的属性时,Jackson 会将类的实例指定为其。Object
com.fasterxml.jackson.databind.deser.std.UntypedObjectDeserializer
Deserializer
在您的例子中,这个类将首先处理数组,因此我们获得一个作为主要的反序列化结果。List
这将由类的实例组成。为什么?因为在它们的实现中,对于数组中的每个 JSON 对象,它们都会生成一个 with 键、JSON 对象属性名称(按它们出现的顺序命名)和 values(一个带有 JSON 对象属性值的值)。List
LinkedHashMap
LinkedHashMap
String[]
此过程由该方法处理。如果您有某种字段允许您识别正在处理的类型,也许您可以定义一个新的字段,该字段扩展和覆盖要创建的方法,从其父级返回的对象,实际的、具体的对象,只需应用 Java Bean 内省和属性设置器即可。mapObject
Object
Deserializer
UntypedObjectDeserializer
mapObject
Map
最后一种方法也可以用 s 来实现。它们实际上非常适合这个两阶段过程。这背后的想法与上一段中描述的相同:您将收到 a of s,如果您能够识别具体类型,则只需根据收到的信息构建相应的实例即可。您甚至可以用于最后的转换步骤。Converter
List
Map
ObjectMapper
评论
Object
不幸的是,反序列化程序接受字段声明类型中的类型。因此,如果 field 是,那么它不会反序列化为任何内容并将其保留为 .对此没有简单的解决方案,因为反序列化没有关于它应该是什么类型的信息。一种解决方案是自定义映射器,就像答案之一一样,或者您可以使用自定义类而不是我用于类似用例的类:Object
Map
TaggedObject
Object
public class TaggedObject {
@Expose
private String type;
@Expose
private Object value;
@Expose
private Pair<TaggedObject, TaggedObject> pairValue;
@SuppressWarnings("unchecked")
public TaggedObject(Object v) {
this.type = getTypeOf(v);
if (v instanceof Pair) {
this.pairValue = tagPair((Pair<Object, Object>) v);
this.value = null;
} else if ("long".equals(type)){
this.value = "" + v;
this.pairValue = null;
} else {
this.value = v;
this.pairValue = null;
}
}
private static Pair<TaggedObject, TaggedObject> tagPair(Pair<Object, Object> v) {
return new Pair<TaggedObject, TaggedObject>(TaggedObject.tag(v.first), TaggedObject.tag(v.second));
}
private static String getTypeOf(Object v) {
Class<?> cls = v.getClass();
if (cls.equals(Double.class))
return "double";
if (cls.equals(Float.class))
return "float";
if (cls.equals(Integer.class))
return "integer";
if (cls.equals(Long.class))
return "long";
if (cls.equals(Byte.class))
return "byte";
if (cls.equals(Boolean.class))
return "boolean";
if (cls.equals(Short.class))
return "short";
if (cls.equals(String.class))
return "string";
if (cls.equals(Pair.class))
return "pair";
return "";
}
public static TaggedObject tag(Object v) {
if (v == null)
return null;
return new TaggedObject(v);
}
public Object get() {
if (type.equals("pair"))
return new Pair<Object, Object>(
untag(pairValue.first),
untag(pairValue.second)
);
return getAsType(value, type);
}
private static Object getAsType(Object value, String type) {
switch (type) {
case "string" : return value.toString();
case "double" : return value;
case "float" : return ((Double)value).doubleValue();
case "integer": return ((Double)value).intValue();
case "long" : {
if (value instanceof Double)
return ((Double)value).longValue();
else
return Long.parseLong((String) value);
}
case "byte" : return ((Double)value).byteValue();
case "short" : return ((Double)value).shortValue();
case "boolean": return value;
}
return null;
}
public static Object untag(TaggedObject object) {
if (object != null)
return object.get();
return null;
}
}
这是针对 google gson(这就是为什么有注释的原因),但应该可以与 jackson 一起使用。我没有包括类,但你肯定可以通过签名来制作自己的类或省略它(我需要序列化对)。@Expose
Pair
评论
要求是将 Object an 类型的属性反序列化为正确的类。
可以考虑对不同类型的人使用不同的路线。如果这不是一个选项,并且由于您可以控制序列化数据并尽可能接近现有代码,则可以使用 JsonDeserialize 批注。
解决方案可能是向 JSON 数据添加“response_type”。
由于除了动物列表之外,您还需要反序列化其他类型的对象,因此也许使用更通用的名称而不是动物是有意义的。让我们将其从 重命名为 。animals
response
所以代替
private Object animals;
您将拥有:
private Object response;
然后,您的代码稍微修改到上述几点可能如下所示:
package jackson.poly;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
public class Zoo {
private String responseType;
@JsonDeserialize(using = ResponseDeserializer.class)
private Object response;
public String getResponseType() { return responseType; }
public Object getResponse() { return response; }
}
ResponseDeserializer 可能如下所示:
package jackson.poly;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import java.io.IOException;
import java.util.Arrays;
public class ResponseDeserializer extends JsonDeserializer<Object> {
@Override
public Object deserialize(JsonParser jsonParser,
DeserializationContext deserializationContext) throws
IOException, JsonProcessingException {
Zoo parent = (Zoo)deserializationContext.getParser().getParsingContext().getParent().getCurrentValue();
String responseType = parent.getResponseType();
if(responseType.equals("Animals[]")) {
Animal[] animals = jsonParser.readValueAs(Animal[].class);
return Arrays.asList(animals);
}
else if(responseType.equals("Facilities[]")) {
Facility[] facilities = jsonParser.readValueAs(Facility[].class);
return Arrays.asList(facilities);
}
throw new RuntimeException("unknown subtype " + responseType);
}
}
使用如下输入数据(请注意 response_type 属性):
private static final String JSON_STRING = "{\n"
+ " \"response_type\": \"Animals[]\",\n"
+ " \"response\": [\n"
+ " {\"name\": \"dog\"},\n"
+ " {\"name\": \"cat\"}\n"
+ " ]\n"
+ "}";
上述程序的测试运行将在调试控制台上生成以下输出:
class jackson.poly.Dog
class jackson.poly.Cat
如果你使用这样的东西作为输入(假设相应的类以类似于动物类的形式存在):
private static final String JSON_STRING = "{\n"
+ " \"response_type\": \"Facilities[]\",\n"
+ " \"response\": [\n"
+ " {\"name\": \"house\"},\n"
+ " {\"name\": \"boat\"}\n"
+ " ]\n"
+ "}";
你会得到
class jackson.poly.House
class jackson.poly.Boat
作为输出。
如果不使 animals 变量为 List 类型,就很难让对象映射器理解存在为 animals 定义的子类型。
如果没有,请编写自己的序列化程序,如下所示。
class AnimalsDeserializer extends StdConverter<List<Animal>, Object> {
@Override
public Object convert(List<Animal> animals) {
return animals;
}
}
用@JsonDeserialize注释动物。
@Data
class Zoo {
@JsonDeserialize(converter = AnimalsDeserializer.class)
private Object animals;
}
那么它肯定会起作用。
评论
我向所有参与的人表示衷心的感谢。我决定采用一点内省的方法,并编写了自定义反序列化程序,该反序列化程序会查看列表中的第一个对象,以确定是否需要将其反序列化为List。
public class AnimalsDeserializer extends JsonDeserializer<Object> {
private Set<String> subtypes = Set.of("dog", "cat");
@Override
public Object deserialize(JsonParser parser, DeserializationContext ctx)
throws IOException, JsonProcessingException {
if (parser.getCurrentToken() == JsonToken.START_ARRAY) {
TreeNode node = parser.getCodec().readTree(parser);
TreeNode objectNode = node.get(0);
if (objectNode == null) {
return List.of();
}
TreeNode type = objectNode.get("name");
if (type != null
&& type.isValueNode()
&& type instanceof TextNode
&& subtypes.contains(((TextNode) type).asText())) {
return parser.getCodec().treeAsTokens(node).readValueAs(new TypeReference<ArrayList<Animal>>() {
});
}
}
return parser.readValueAs(Object.class);
}
}
也许有些检查是多余的。它有效,:)才是最重要的。 斯蒂芬·施莱赫特(StephanSchlecht)是最具鼓舞人心的答案的作者。再次感谢!
评论
List<? extends Animal>
Zoo.animals
List<LinkedHashMap>
MixIn
Animal
animals
Object