如何配置Jackson以默认键入反序列化命名类型?

时间:2016-07-14 17:18:30

标签: java jackson

考虑以下示例:

DateFormat

反序列化在运行时失败,但有以下异常: package com.example; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeInfo.Id; import com.fasterxml.jackson.annotation.JsonTypeName; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping; public class JacksonDeserializationOfNamedTypes { public static void main(String[] args) throws Exception { ObjectMapper jackson = new ObjectMapper(); jackson.enableDefaultTypingAsProperty(DefaultTyping.JAVA_LANG_OBJECT, "@type"); Balloon redBalloon = new Balloon("red"); String json = jackson.writeValueAsString(redBalloon); //{"@type":"Balloon","color":"red"} //assume the JSON could be anything Object deserialized = jackson.readValue(json, Object.class); assert deserialized instanceof Balloon; assert redBalloon.equals(deserialized); } @JsonTypeName("Balloon") @JsonTypeInfo(use = Id.NAME) public static final class Balloon { private final String color; //for deserialization private Balloon() { this.color = null; } public Balloon(final String color) { this.color = color; } public String getColor() { return color; } @Override public boolean equals(final Object obj) { if (this == obj) return true; if (obj == null || getClass() != obj.getClass()) return false; final Balloon other = (Balloon) obj; return this.color.equals(other.color); } @Override public int hashCode() { int result = color.hashCode(); result = 31 * result + color.hashCode(); return result; } @Override public String toString() { return color + " balloon"; } } }

生成的JSON肯定具有Jackson正确确定类型所需的所有信息,因此如何配置ObjectMapper以将Exception in thread "main" java.lang.IllegalArgumentException: Invalid type id 'Balloon' (for id type 'Id.class'): no such class found正确映射到"Balloon"

5 个答案:

答案 0 :(得分:3)

我目前的解决方案是将自定义反序列化器和手动形成的类型名称映射组合到Java类型中:

package com.example.jackson;

import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeName;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.ObjectCodec;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.fasterxml.jackson.databind.module.SimpleModule;

import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

public class JacksonDeserializerOfNamedTypes extends StdDeserializer<Object> {
    private final Map<String, Class<?>> typesByName;
    private final String typeProperty;

    private JacksonDeserializerOfNamedTypes(final Map<String, Class<?>> typesByName, final String typeProperty) {
        super(Object.class);

        this.typesByName = typesByName;
        this.typeProperty = typeProperty;
    }

    @Override
    public Object deserialize(final JsonParser parser, final DeserializationContext context) throws IOException, JsonProcessingException {
        final ObjectCodec codec = parser.getCodec();
        final JsonNode root = parser.readValueAsTree();
        final JsonNode typeNameNodeOrNull = root.get(typeProperty);
        if (typeNameNodeOrNull == null) {
            throw new JsonMappingException(parser, "Unable to determine Java type of JSON: " + root);
        } else {
            final String typeName = typeNameNodeOrNull.asText();
            return Optional
                .ofNullable(typesByName.get(typeName))
                .map(type -> parseOrNull(root, type, codec))
                .orElseThrow(() ->
                    new JsonMappingException(parser, String.format(
                        "Unsupported type name '%s' in JSON: %s", typeName, root)));
        }
    }

    private <T> T parseOrNull(final JsonNode root, final Class<T> type, final ObjectCodec codec) {
        try {
            return root.traverse(codec).readValueAs(type);
        } catch (IOException e) {
            return null;
        }
    }

    public static void main(String[] args) throws Exception {
        final Map<String, Class<?>> typesByName = scanForNamedTypes();

        final SimpleModule namedTypesModule = new SimpleModule("my-named-types-module");
        namedTypesModule.addDeserializer(Object.class, new JacksonDeserializerOfNamedTypes(typesByName, JsonTypeInfo.Id.NAME.getDefaultPropertyName()));

        final Car pinto = new Car("Ford", "Pinto", 1971);
        final Balloon sharik = new Balloon("blue");
        final ObjectMapper mapper = new ObjectMapper().registerModule(namedTypesModule);
        System.out.println(mapper.readValue(mapper.writeValueAsString(pinto), Object.class).getClass());
        System.out.println(mapper.readValue(mapper.writeValueAsString(sharik), Object.class).getClass());
    }

    @JsonTypeName("Balloon")
    @JsonTypeInfo(use = JsonTypeInfo.Id.NAME)
    public static final class Balloon {
        public String color;

        private Balloon() {}

        public Balloon(final String color) {
            this.color = color;
        }
    }

    @JsonTypeName("Car")
    @JsonTypeInfo(use = JsonTypeInfo.Id.NAME)
    public static final class Car {
        public String make;
        public String model;
        public int year;

        private Car() {}

        public Car(final String make, final String model, final int year) {
            this.make = make;
            this.model = model;
            this.year = year;
        }
    }

    static Map<String, Class<?>> scanForNamedTypes() {
        //in reality, i'd be using a framework (e.g. Reflections) to scan the classpath
        //for classes tagged with @JsonTypeName to avoid maintaining manual mappings
        final Map<String, Class<?>> typesByName = new HashMap<>();
        typesByName.put("Balloon", Balloon.class);
        typesByName.put("Car", Car.class);
        return Collections.unmodifiableMap(typesByName);
    }
}

答案 1 :(得分:2)

  

制作的JSON肯定拥有杰克逊需要的所有信息   正确确定类型

您已向杰克逊提供以下

Object deserialized = jackson.readValue(json, Object.class);

Object是所有Java引用类型的超类型。这是众所周知的。对杰克逊不是很有用。您的JSON还包含

{"@type":"Balloon","color":"red"}

鉴于此,并感谢

jackson.enableDefaultTypingAsProperty(DefaultTyping.JAVA_LANG_OBJECT, "@type");

杰克逊可以推断它可以使用@type元素的值。但是,名称Balloon可以做些什么呢?杰克逊不知道类路径上的所有类型。您是否拥有完全限定名称Balloon的类型?您是否有名为com.example.Balloon的类型或名为org.company.toys.Balloon的类型?杰克逊应该如何选择?

@JsonTypeInfo和注释族通常用于继承层次结构的反序列化。例如

@JsonTypeInfo(use = Id.NAME)
@JsonSubTypes(value = @Type(value = Balloon.class))
public abstract static class Toy {

}

@JsonTypeName("Balloon")
public static final class Balloon extends Toy {

Object deserialized = jackson.readValue(json, Toy.class);

现在Jackson可以查找Toy类及其元数据,将其子类标识为Balloon,它还可以检查名称Balloon

如果您没有尝试为继承层次结构建模,那么最简单的解决方案 here 将使用Balloon@JsonTypeName类的完全限定名称注释。

@JsonTypeName("com.example.JacksonDeserializationOfNamedTypes$Balloon")
@JsonTypeInfo(use = Id.NAME)
public static final class Balloon {

该名称将出现在JSON中,Jackson将使用它来确定目标类别。

答案 2 :(得分:0)

实现动态类型解析的另一种方法是使用注释@JsonTypeIdResolver和接口TypeIdResolver的自定义实现(有基本抽象实现com.fasterxml.jackson.databind.jsontype.impl.TypeIdResolverBase)。

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "jsonPropertyWithSomeIdToReferClass")
@JsonTypeIdResolver(YourTypeIdResolver.class)
class YourBaseClass{}

示例:

  • Tutorial
  • Jackson库中的实现:com.fasterxml.jackson.databind.jsontype.impl.TypeNameIdResolver

reflection库也可以帮助在类路径中查找类:

Reflections reflections = new Reflections("my.project");
Set<Class<? extends SomeType>> subTypes = reflections.getSubTypesOf(SomeType.class);
Set<Class<?>> annotated = reflections.getTypesAnnotatedWith(SomeAnnotation.class);

为避免在代码中添加与杰克逊多态功能相关的漏洞,可以帮助诸如this之类的文章。

答案 3 :(得分:0)

SWince 我刚刚偶然发现了同样的问题,我想在解决方案中添加 aboce 以提供完全限定的名称,您不需要每次都输入它,但您可以这样做:

import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeInfo.As;
import com.fasterxml.jackson.annotation.JsonTypeInfo.Id;

@JsonTypeInfo(use = Id.CLASS, include = As.WRAPPER_OBJECT, property = "@class")
public interface JsonStorable {}

然后你可以设置你的映射器

ObjectMapper objectMapper = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT);

这就是我要做的。它甚至可以处理多态类。除了 WRAPPER_OBJECT,您还可以使用上面提到的属性。结果是这样的

{
  "de.firegate.jsonDb.User" : {
    "username" : "user",
    "password" : "pass",
    "createdAt" : "1970-01-01T12:00:00",
    "usergroups" : [ "Editor", "Author" ],
    "pets" : [ {
      "de.firegate.jsonDb.Cat" : {
        "name" : "Maunz",
        "favoriteToy" : "Bell"
      }
    }, {
      "de.firegate.jsonDb.Dog" : {
        "name" : "Wuff",
        "breed" : "Sheppard",
        "medals" : 7
      }
    }, {
      "de.firegate.jsonDb.special.Bird" : {
        "name" : "Butschi"
      }
    } ]
  }
}

答案 4 :(得分:-1)

总之(太迟了:))

你应该:

  • 提供完全限定的className,因此Jackson可以找到要反序列化的类,例如:

    @JsonTypeName("com.example.JacksonDeserializationOfNamedTypes$Balloon")
    
  • add a custom deserializer处理您的Balloon类型