Jackson中具有通用包装类的自定义(反)序列化器

时间:2018-09-04 22:36:54

标签: java json serialization jackson deserialization

我正在制作一个通用的列表包装器,其中包含元信息和实际数据(列表)。这是Vert.x项目,由于EventBus的原因,包装程序必须支持序列化到JSON和从JSON反序列化。

简单用法:

Wrapper<SomeType> wrapper = new Wrapper<>(metaInfo, list);

我认为使用Jackson可能是最简单的方法(因为Vert.x在后台也使用Jackson)。

SomeType可以是我的团队使用的任何类型,可以是生成的POJO或某种库类型(例如Vert.x中的JsonObject)。这很重要,因为我们无法更改该类型(例如,添加默认构造函数,getter / setter等)或对其进行注释(例如,@JsonProperty@JsonCreator等),仅在需要时编写自定义(反)序列化器

要能够将JSON反序列化为适当的类型,还需要一些其他信息,而对于Jackson,可以通过@JsonTypeInfo实现。因此,我的(简化此问题)包装器类如下:

class Wrapper<T> {
    @JsonProperty("info")
    private String someInfo;
    @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, property = "@class")
    private List<T> data;

    @JsonCreator
    public Wrapper(@JsonProperty("info") String someInfo, @JsonProperty("data") List<T> data) {
        this.someInfo = someInfo;
        this.data = data;
    }

    public static <T> Wrapper<T> fromJson(String json) {
        if (json == null) {
            return null;
        }
        ObjectMapper mapper = new ObjectMapper();
        SimpleModule module = new SimpleModule();
        //in test, I've hard coded deserializer for ProblematicClass
        module.addDeserializer(ProblematicClass.class, new ProblematicClassDeserializer());
        mapper.registerModule(module);

        try {
            return mapper.readValue(json, Wrapper.class);
        } catch (IOException e) {
            throw new IllegalArgumentException("Cannot deserialize Wrapper from Json. Message: " + e.getMessage(), e);
        }
    }

    public String toJson() {
        ObjectMapper mapper = new ObjectMapper();
        SimpleModule module = new SimpleModule();
        //in test, I've hard coded deserializer for ProblematicClass
        module.addSerializer(ProblematicClass.class, new ProblematicClassSerializer());
        mapper.registerModule(module);
        try {
            return mapper.writeValueAsString(this);
        } catch (JsonProcessingException e) {
            throw new IllegalStateException("Cannot serialize Wrapper to Json. Message: " + e.getMessage(), e);
        }
    }
}

如果类型为“ Jackson可序列化”(例如具有默认构造函数,getter / setter等),则一切正常,但如果不成功,则会出现问题。这是一个有问题的示例类。我将此类编写为Vert.x JsonObject的测试别名,因此MVCE很小,不需要Vert.x依赖项。

class ProblematicClass {
    private Map<String, Object> map;

    public ProblematicClass() {
        this.map = new HashMap<>();
    }

    public Boolean isEmpty() {
        return map.isEmpty();
    }

    public Map<String, Object> getMap() {
        return map;
    }

    public ProblematicClass put(String key, String value) {
        map.put(key, value);
        return this;
    }

    public ProblematicClass put(String key, Integer value) {
        map.put(key, value);
        return this;
    }
    //put (..., boolean), put (..., Float) etc.

    public String toJson() throws JsonProcessingException {
        //e.g. some internal logic (as for JsonObject)
        //this is just example (I know that everything is written as string)
        ObjectMapper mapper = new ObjectMapper();
        return mapper.writeValueAsString(map);
    }

    public static ProblematicClass fromJson(String json) throws IOException {
        ProblematicClass instance = new ProblematicClass();
        ObjectMapper mapper = new ObjectMapper();
        instance.map = (Map<String, Object>) mapper.readValue(json, Map.class);
        return instance;
    }
    //getString, getInteger, getFloat etc.
}

此类显然需要自定义序列化器/反序列化器,因为它具有例如isEmpty不能反序列化。请记住,我们“无法”更改此类,例如来自外部库。

因此,此代码成功序列化(生成错误的JSON),并在反序列化失败:

List<ProblematicClass> list = new ArrayList<>();
list.add(ProblematicClass.fromJson("{\"a\": \"10\"}"));
list.add(ProblematicClass.fromJson("{\"b\": \"20\"}"));
Wrapper<ProblematicClass> wrapper = new Wrapper<>("info", list);
String json = wrapper.toJson();

产生的JSON看起来像这样:

{"data":[{"@class":"com.test.ProblematicClass","map":{"a":"10"},"empty":false},
{"@class":"com.test.ProblematicClass","map":{"b":"20"},"empty":false}],"info":"info"}

并且它不可反序列化,因为empty JSON密钥。因此,我们需要编写序列化器和反序列化器:

class ProblematicClassSerializer extends StdSerializer<ProblematicClass> {
    public ProblematicClassSerializer() {
        this(null);
    }

    protected ProblematicClassSerializer(Class<ProblematicClass> t) {
        super(t);
    }

    @Override
    public void serializeWithType(ProblematicClass value, JsonGenerator gen, SerializerProvider serializers, TypeSerializer typeSer) throws IOException {
        WritableTypeId typeId = typeSer.typeId(value, START_OBJECT);
        typeSer.writeTypePrefix(gen, typeId);
        gen.writeFieldName("@json");
        serialize(value, gen, serializers);  //our custom serialize method written below
        typeSer.writeTypeSuffix(gen, typeId);
    }

    @Override
    public void serialize(ProblematicClass value, JsonGenerator jgen, SerializerProvider serializerProvider) throws IOException {
        jgen.writeRawValue(value == null ? null : value.toJson());
    }
}

class ProblematicClassDeserializer extends StdDeserializer<ProblematicClass> {
    public ProblematicClassDeserializer() {
        this(null);
    }

    protected ProblematicClassDeserializer(Class<?> vc) {
        super(vc);
    }

    @Override
    public ProblematicClass deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
        String json = jsonParser.getCodec().readTree(jsonParser).toString(); //this is actual json representation
        ObjectMapper mapper = new ObjectMapper();
        Map<String, Object> map = mapper.readValue(json, Map.class); //I know this is redundant, but for example it is fine
        return json == null ? null : ProblematicClass.fromJson(mapper.writeValueAsString(map.get("@json")));
        //sometimes I got {"key": "value"} //when serialize is called (ProblematicClass wrapped)
        //and sometimes I got {"@json": {"key": "value"}} //when using bare ProblematicClass as in Wrapper<ProblematicClass>
        //I need to distinguish these two cases but I don't know when is te first and when second
        //because deserializationContext is not giving me these information
    }
}

据我了解,当serialize是另一个类的属性(例如,写在下面的ProblematicClass时,将调用SomeRealClass,并且得到的JSON将是{"pclass": {"a": 10}},这是可以的。因此,我在jgen.writeRawValue中使用了serialize

class SomeRealClass {
    public ProblematicClass pclass = ProblematicClass.fromJson("{\"a\": \"10\"}");
    public Integer i1 = 10, i2 = 20;

    SomeRealClass() throws IOException {
    }
}

serializeWithType是在我单独序列化ProblematicClass时使用的,然后生成的JSON将是{"a": 10},这也是可以的。

但是,由于我在@JsonTypeInfo中使用了Wrapper,因此Jackson注入了类型信息("@class": "com.test.ProblematicClass"),这导致第二个示例({"a": 10})出现问题。因此,在serializeWithType中,我写了gen.writeFieldName("@json");来“包装” JSON,因此它应该看起来像这样(包含Jackson的元信息):

{"info":"info","data":[{"@class":"com.test.ProblematicClass","@json":{"a":"10"}},
{"@class":"com.test.ProblematicClass","@json":{"b":"20"}}]}

这很好,直到反序列化为止。我无法区分使用的是哪种类型的序列化,一种没有@json(例如"pclass": {"a": 10})还是带有@json的序列化。在前一种情况下(调用map.get("@json")时,null返回serialize

说了这么多,上面有详细说明的代码:

public static void main(String[] args) throws IOException {
    //without (de)serializes this does not work as it's json is not deserializable (because of 'empty' key)
    //   {"data":[{"@class":"com.test.ProblematicClass","map":{"a":"10"},"empty":false},
    //     {"@class":"com.test.ProblematicClass","map":{"b":"20"},"empty":false}],"info":"info"}
    //when I hardcode (de)serializer then this (correct) json is produced:
    //   {"info":"info","data":[{"@class":"com.test.ProblematicClass","@json":{"a":"10"}},
    //     {"@class":"com.test.ProblematicClass","@json":{"b":"20"}}]}
    {
        List<ProblematicClass> list = new ArrayList<>();
        list.add(ProblematicClass.fromJson("{\"a\": \"10\"}"));
        list.add(ProblematicClass.fromJson("{\"b\": \"20\"}"));
        Wrapper<ProblematicClass> wrapper = new Wrapper<>("info", list);
        String json = wrapper.toJson();
        Wrapper<ProblematicClass> copy = Wrapper.fromJson(json);
    }
    //---------------------------------------------------------
    //but, when container class is used (SomeRealClass)
    //this @json is redundant, so I wrote it only in serializeWithType
    //resulting JSON is correct? there is no @json, like: "problematicClass": {"@json": {"a": "10}}
    //and deserialization fails (map.get("@json") is null)
    //    {"info":"info-real","data":[{"@class":"com.test.SomeRealClass","problematicClass":{"a":"10"},"i1":10,"i2":20},
    //       {"@class":"com.test.SomeRealClass","problematicClass":{"a":"10"},"i1":10,"i2":20}]}
    {
        List<SomeRealClass> list = new ArrayList<>();
        list.add(new SomeRealClass());
        list.add(new SomeRealClass());
        Wrapper<SomeRealClass> wrapper = new Wrapper<>("info-real", list);
        String json = wrapper.toJson();
        Wrapper<SomeRealClass> copy = Wrapper.fromJson(json);
    }
}

这是pastebin上可正常使用的MVCE。

关于如何解决此问题的任何建议?但是,请不要建议“总是在包装器中包装类型”,例如class ProblematicClassHolder {public ProblematicClass pclass; }

非常感谢您阅读这个冗长的问题,当然也感谢所有评论和答案。

0 个答案:

没有答案