将多态对象的列表反序列化为对象字段

huangapple go评论71阅读模式
英文:

Deserialize List of polymorphic objects into Object field

问题

My REST endpoint returns Response object containing Object field. Everything is fine with serialization but when I started to write client for this API I encountered an issue with deserialization. I made this example code based on some questions/articles about polymorphic serialization with Jackson. It demonstrates the issue.

@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());
        }
    }
}

What I expect(and what I have with List<Animals> as Zoo#animals type) in output:

class jackson.poly.Dog
class jackson.poly.Cat

What I have now with Object:

class java.util.LinkedHashMap
class java.util.LinkedHashMap

But I need to deserialize other types of objects besides a list of animals. Help please!

英文:

My REST endpoint returns Response object containing Object field. Everything is fine with serialization but when I started to write client for this API I encountered an issue with deserialization. I made this example code based on some questions/articles about polymorphic serialization with Jackson. It demonstrates the issue.

@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 = &quot;name&quot;, visible = true)
@JsonSubTypes({
        @JsonSubTypes.Type(name = &quot;dog&quot;, value = Dog.class),
        @JsonSubTypes.Type(name = &quot;cat&quot;, value = Cat.class)
})
public class Mixin {

}

public class Main {

    private static final String JSON_STRING = &quot;{\n&quot;
            + &quot;  \&quot;animals\&quot;: [\n&quot;
            + &quot;    {\&quot;name\&quot;: \&quot;dog\&quot;},\n&quot;
            + &quot;    {\&quot;name\&quot;: \&quot;cat\&quot;}\n&quot;
            + &quot;  ]\n&quot;
            + &quot;}&quot;;

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

What I expect(and what I have with List&lt;Animals&gt; as Zoo#animals type) in output:

class jackson.poly.Dog
class jackson.poly.Cat

What I have now with Object:

class java.util.LinkedHashMap
class java.util.LinkedHashMap

But I need to deserialize other types of objects besides list of animals. Help please!

答案1

得分: 0

如果您知道要转换的类型,可以使用ObjectMapperconvertValue方法。

您可以转换单个值或整个列表。

考虑以下示例:

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。不必从头开始创建,可以重用一些代码。

当您将属性定义为Object时,Jackson将分配一个com.fasterxml.jackson.databind.deser.std.UntypedObjectDeserializer类的实例作为其Deserializer

在您的情况下,这个类将首先处理数组,因此我们将获得一个List作为主要反序列化结果。

这个List将由LinkedHashMap类的实例组成。为什么?因为在它们的实现中,对于数组中的每个JSON对象,它们生成一个LinkedHashMap,其中包含键,JSON对象属性名称按其出现顺序排列,以及值,一个包含JSON对象属性值的String[]

这个过程由mapObject方法处理。如果您有某种字段可以用来识别正在处理的Object类型,也许您可以定义一个新的Deserializer,它扩展了UntypedObjectDeserializer并覆盖了mapObject方法,以从其父类返回的Map中创建实际的具体对象,只需应用Java Bean内省和属性设置器。

这种最后的方法也可以使用Converter来实现。它们实际上非常适合这个两阶段的过程。其背后的想法与上一段中描述的相同:您将收到一系列MapList,如果能够识别具体类型,只需从接收到的信息构建相应的实例。您甚至可以使用ObjectMapper来进行最后的转换步骤。

英文:

If you know the type to convert to, you can use the method convertValue of ObjectMapper.

You can convert either a single value or the whole list.

Consider the following example:

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&lt;Animal&gt; animals = objectMapper.convertValue(zoo.getAnimals(), new TypeReference&lt;List&lt;Animal&gt;&gt;(){});
  for (Object animal : animals) {
    System.out.println(animal.getClass());
  }
}

For your comments, if you are unsure of the actual type that need to be processed, one option that can be useful is enabling Jackson's default typing (with the required precautions).

You can do it like this:

PolymorphicTypeValidator validator = BasicPolymorphicTypeValidator
  .builder()
  .allowIfBaseType(Animal.class)
  .build()
;

ObjectMapper mapper = new ObjectMapper();
mapper.activateDefaultTyping(DefaultTyping.NON_FINAL, validator);

Another think you can do is define a custom Deserializer for the field that contains your arbitrary information.

Instead of creating one from scratch, you can reuse some code.

When you define as Object your property, Jackson will assign an instance of the class com.fasterxml.jackson.databind.deser.std.UntypedObjectDeserializer as itsDeserializer.

In your case, this class will process first the array, so we obtain a List as the main deserialization result.

This List will be composed of instances of the class LinkedHashMap. Why? Because in their implementation, for every JSON object in the array, they generate a LinkedHashMap with keys, the JSON object property names in the order that they appear, and values, an String[] with the JSON object property values.

This process is handled by the mapObject method. If you have some kind of field that allow you to identify the type of Object you are processing, maybe you can define a new Deserializer that extends UntypedObjectDeserializer and overwrites the mapObject method to create, from the Map returned by its parent, the actual, concrete, objects, just by applying Java Bean introspection and property setters.

This last approach can also be implemented with Converters. They are actually very well suited for this two phase process. The idea behind that is the same described in the last paragraph: you will receive a List of Maps, and if you are able to identify the concrete type, you only need to build the corresponding instances from the information received. You can even use ObjectMapper for this last conversion step.

答案2

得分: 0

抱歉,以下是翻译好的部分:

很不幸,反序列化器接受来自字段声明类型的类型。因此,如果字段是Object,它将不会反序列化为任何内容,并将其保留为Map。由于反序列化器无法获取关于应该是什么类型的信息,因此没有简单的解决方法。一个解决方案是自定义映射器,就像其中一个答案中所示,或者您可以使用自定义的TaggedObject类,而不是Object,这是我用于类似用例的方式:

public class TaggedObject {

	@Expose
	private String type;
	@Expose
	private Object value;
	@Expose
	private Pair&lt;TaggedObject, TaggedObject&gt; pairValue;
	
	@SuppressWarnings(&quot;unchecked&quot;)
	public TaggedObject(Object v) {
		this.type = getTypeOf(v);
		if (v instanceof Pair) {
			this.pairValue = tagPair((Pair&lt;Object, Object&gt;) v);
			this.value = null;
		} else if (&quot;long&quot;.equals(type)){
			this.value = &quot;&quot; + v;
			this.pairValue = null;
		} else {
			this.value = v;
			this.pairValue = null;
		}
	}

	private static Pair&lt;TaggedObject, TaggedObject&gt; tagPair(Pair&lt;Object, Object&gt; v) {
		return new Pair&lt;TaggedObject, TaggedObject&gt;(TaggedObject.tag(v.first), TaggedObject.tag(v.second));
	}

	private static String getTypeOf(Object v) {
		Class&lt;?&gt; cls = v.getClass();
		if (cls.equals(Double.class))
			return &quot;double&quot;;
		if (cls.equals(Float.class))
			return &quot;float&quot;;
		if (cls.equals(Integer.class))
			return &quot;integer&quot;;
		if (cls.equals(Long.class))
			return &quot;long&quot;;
		if (cls.equals(Byte.class))
			return &quot;byte&quot;;
		if (cls.equals(Boolean.class))
			return &quot;boolean&quot;;
		if (cls.equals(Short.class))
			return &quot;short&quot;;
		if (cls.equals(String.class))
			return &quot;string&quot;;
		if (cls.equals(Pair.class))
			return &quot;pair&quot;;
		return &quot;&quot;;
	}

	public static TaggedObject tag(Object v) {
		if (v == null)
			return null;
		return new TaggedObject(v);
	}

	public Object get() {
		if (type.equals(&quot;pair&quot;))
			return new Pair&lt;Object, Object&gt;(
					untag(pairValue.first),
					untag(pairValue.second)
					);
		return getAsType(value, type);
	}

	private static Object getAsType(Object value, String type) {
		switch (type) {
		case &quot;string&quot; : return value.toString();
		case &quot;double&quot; : return value;
		case &quot;float&quot;  : return ((Double)value).doubleValue();
		case &quot;integer&quot;: return ((Double)value).intValue();
		case &quot;long&quot;   : {
			if (value instanceof Double)
				return ((Double)value).longValue();
			else
				return Long.parseLong((String) value);
		}
		case &quot;byte&quot;   : return ((Double)value).byteValue();
		case &quot;short&quot;  : return ((Double)value).shortValue();
		case &quot;boolean&quot;: return value;
		}
		return null;
	}

	public static Object untag(TaggedObject object) {
		if (object != null)
			return object.get();
		return null;
	}
}

这是为Google Gson准备的(这就是为什么有@Expose注解),但应该在Jackson中运行正常。我没有包含Pair类,但您可以根据签名创建自己的类或省略它(我需要序列化成对)。

英文:

Unfortunately deserializer accepts type from the declared type of the field. So if field is Object then it will not deserialize to anything and leave it as Map. There is no simple solution to this because deserializes has no information about what type it should be. One solution is custom mapper like one of the answers is, or you can use custom TaggedObject class instead of Object which is what I used for similar use case:

public class TaggedObject {

	@Expose
	private String type;
	@Expose
	private Object value;
	@Expose
	private Pair&lt;TaggedObject, TaggedObject&gt; pairValue;
	
	@SuppressWarnings(&quot;unchecked&quot;)
	public TaggedObject(Object v) {
		this.type = getTypeOf(v);
		if (v instanceof Pair) {
			this.pairValue = tagPair((Pair&lt;Object, Object&gt;) v);
			this.value = null;
		} else if (&quot;long&quot;.equals(type)){
			this.value = &quot;&quot; + v;
			this.pairValue = null;
		} else {
			this.value = v;
			this.pairValue = null;
		}
	}

	private static Pair&lt;TaggedObject, TaggedObject&gt; tagPair(Pair&lt;Object, Object&gt; v) {
		return new Pair&lt;TaggedObject, TaggedObject&gt;(TaggedObject.tag(v.first), TaggedObject.tag(v.second));
	}

	private static String getTypeOf(Object v) {
		Class&lt;?&gt; cls = v.getClass();
		if (cls.equals(Double.class))
			return &quot;double&quot;;
		if (cls.equals(Float.class))
			return &quot;float&quot;;
		if (cls.equals(Integer.class))
			return &quot;integer&quot;;
		if (cls.equals(Long.class))
			return &quot;long&quot;;
		if (cls.equals(Byte.class))
			return &quot;byte&quot;;
		if (cls.equals(Boolean.class))
			return &quot;boolean&quot;;
		if (cls.equals(Short.class))
			return &quot;short&quot;;
		if (cls.equals(String.class))
			return &quot;string&quot;;
		if (cls.equals(Pair.class))
			return &quot;pair&quot;;
		return &quot;&quot;;
	}

	public static TaggedObject tag(Object v) {
		if (v == null)
			return null;
		return new TaggedObject(v);
	}

	public Object get() {
		if (type.equals(&quot;pair&quot;))
			return new Pair&lt;Object, Object&gt;(
					untag(pairValue.first),
					untag(pairValue.second)
					);
		return getAsType(value, type);
	}

	private static Object getAsType(Object value, String type) {
		switch (type) {
		case &quot;string&quot; : return value.toString();
		case &quot;double&quot; : return value;
		case &quot;float&quot;  : return ((Double)value).doubleValue();
		case &quot;integer&quot;: return ((Double)value).intValue();
		case &quot;long&quot;   : {
			if (value instanceof Double)
				return ((Double)value).longValue();
			else
				return Long.parseLong((String) value);
		}
		case &quot;byte&quot;   : return ((Double)value).byteValue();
		case &quot;short&quot;  : return ((Double)value).shortValue();
		case &quot;boolean&quot;: return value;
		}
		return null;
	}

	public static Object untag(TaggedObject object) {
		if (object != null)
			return object.get();
		return null;
	}
}

This is for google gson (that's why there are @Expose annotations) but should work just fine with jackson. I did not include Pair class but you can surely make your own based by the signature or omit it (I needed to serialize pairs).

答案3

得分: 0

要求是将类型为Object的属性反序列化为正确的类。

可以考虑为不同类型使用不同的路由。如果这不是一个选项,而且由于您可以控制序列化的数据,并且要尽量保持与现有代码的一致性,您可以使用@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 = "{
  \"response_type\": \"Animals[]\",
  \"response\": [
    {\"name\": \"dog\"},
    {\"name\": \"cat\"}
  ]
}";

上述程序的测试运行将在调试控制台上产生以下输出:

class jackson.poly.Dog
class jackson.poly.Cat

如果您使用类似以下的输入(假设相应的类与动物类类似存在):

private static final String JSON_STRING = "{
  \"response_type\": \"Facilities[]\",
  \"response\": [
    {\"name\": \"house\"},
    {\"name\": \"boat\"}
  ]
}";

您将得到以下输出:

class jackson.poly.House
class jackson.poly.Boat
英文:

The requirement is that a property of type Object an be deserialized into the correct class.

One could consider using different routes for different types. If this is not an option, and since you have control over the serialized data and to stay as close as possible to your existing code, you could use a JsonDeserialize annotation.

The solution could be to add a 'response_type' to the JSON data.

Since you need to deserialize other types of objects besides list of animals, maybe a more generic name instead of animals make sense. Let's rename it from animals to response.

So instead of

private Object animals;

you will have:

private Object response;

Then your code slightly modified to the above points could look like this:

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

The ResponseDeserializer could look like this:

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&lt;Object&gt; {
@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(&quot;Animals[]&quot;)) {
Animal[] animals = jsonParser.readValueAs(Animal[].class);
return Arrays.asList(animals);
}
else if(responseType.equals(&quot;Facilities[]&quot;)) {
Facility[] facilities = jsonParser.readValueAs(Facility[].class);
return Arrays.asList(facilities);
}
throw new RuntimeException(&quot;unknown subtype &quot; + responseType);
}
}

Using input data like this (note the response_type property):

private static final String JSON_STRING = &quot;{\n&quot;
+ &quot;  \&quot;response_type\&quot;: \&quot;Animals[]\&quot;,\n&quot;
+ &quot;  \&quot;response\&quot;: [\n&quot;
+ &quot;    {\&quot;name\&quot;: \&quot;dog\&quot;},\n&quot;
+ &quot;    {\&quot;name\&quot;: \&quot;cat\&quot;}\n&quot;
+ &quot;  ]\n&quot;
+ &quot;}&quot;;

a test run of the above program would produce the following output on the debug console:

class jackson.poly.Dog
class jackson.poly.Cat

If you would use something like this as input instead (assuming the corresponding classes exist in a form similar to the animal classes):

private static final String JSON_STRING = &quot;{\n&quot;
+ &quot;  \&quot;response_type\&quot;: \&quot;Facilities[]\&quot;,\n&quot;
+ &quot;  \&quot;response\&quot;: [\n&quot;
+ &quot;    {\&quot;name\&quot;: \&quot;house\&quot;},\n&quot;
+ &quot;    {\&quot;name\&quot;: \&quot;boat\&quot;}\n&quot;
+ &quot;  ]\n&quot;
+ &quot;}&quot;;

you would get

class jackson.poly.House
class jackson.poly.Boat

as output.

答案4

得分: 0

Here's the translated code portion:

不必将 animals 变量设置为 List<Animal> 类型,这会让对象映射器难以理解 animals 中定义了子类型。

如果不这样做,请按照以下方式编写自己的序列化程序。

class AnimalsDeserializer extends StdConverter&lt;List&lt;Animal&gt;, Object&gt; {

    @Override
    public Object convert(List&lt;Animal&gt; animals) {
        return animals;
    }
}

使用 @JsonDeserialize 对 animals 进行注释。

@Data
class Zoo {

    @JsonDeserialize(converter = AnimalsDeserializer.class)
    private Object animals;
}

然后它肯定会起作用。

英文:

Without making the animals variable be of type List<Animal>, its difficult to let object mapper understand that there are subtypes defined for animals.

If not, write your own serializer as below.

    class AnimalsDeserializer extends StdConverter&lt;List&lt;Animal&gt;, Object&gt; {
@Override
public Object convert(List&lt;Animal&gt; animals) {
return animals;
}
}

Annotate animals with @JsonDeserialize.

    @Data
class Zoo {
@JsonDeserialize(converter = AnimalsDeserializer.class)
private Object animals;
}

Then it will definitely work.

答案5

得分: 0

以下是翻译好的部分:

"I express sincere gratitude to all who participated. I decided to go with a little bit introspective approach and wrote custom deserializer that looks inside first object in list to figure out if it needs to deserialize this as List<Animal>."

"也许有些检查是多余的。它能够正常工作,这就是最重要的 :)"
"奖项颁发给 StephanSchlecht,因为他提供了最具启发性的答案。再次感谢!"

英文:

I express sincere gratitude to all who participated. I decided to go with a little bit introspective approach and wrote custom deserializer that looks inside first object in list to figure out if it needs to deserialize this as List<Animal>.

public class AnimalsDeserializer extends JsonDeserializer&lt;Object&gt; {

    private Set&lt;String&gt; subtypes = Set.of(&quot;dog&quot;, &quot;cat&quot;);

    @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(&quot;name&quot;);
            if (type != null
                    &amp;&amp; type.isValueNode()
                    &amp;&amp; type instanceof TextNode
                    &amp;&amp; subtypes.contains(((TextNode) type).asText())) {
                return parser.getCodec().treeAsTokens(node).readValueAs(new TypeReference&lt;ArrayList&lt;Animal&gt;&gt;() {
                });
            }
        }
        return parser.readValueAs(Object.class);
    }
}

Maybe some checks are redundant. It works and it's all that matters :).
Award goes to StephanSchlecht as author of most inspirational answer. Thanks again!

huangapple
  • 本文由 发表于 2020年8月10日 19:25:17
  • 转载请务必保留本文链接:https://go.coder-hub.com/63339255.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定