Пользовательский (де) сериализатор в Джексоне с универсальным классом-оберткой - PullRequest
0 голосов
/ 05 сентября 2018

Я делаю универсальную оболочку списка, которая содержит метаинформацию и фактические данные (список). Это Vert.x проект, и из-за EventBus эта оболочка должна поддерживать сериализацию и десериализацию из JSON.

Простое использование:

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

Я думал, что использование Джексона может быть самым простым способом (потому что Vert.x также использует Джексона в фоновом режиме).

SomeType может быть любым типом, который использует моя команда, это может быть сгенерированный POJO или некоторый тип библиотеки (такой как JsonObject из Vert.x). Это важно, потому что мы не можем изменить этот тип (например, добавить конструктор по умолчанию, методы получения / установки и т. Д.) Или аннотировать его (например, @JsonProperty, @JsonCreator и т. Д.), Писать только собственные (де) сериализаторы, если это необходимо.

Чтобы иметь возможность десериализовать JSON для соответствующего типа, необходима некоторая дополнительная информация, а с Джексоном это может быть достигнуто с помощью @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);
        }
    }
}

Все работает нормально, если тип "Сериализуется по Джексону" (например, имеет конструктор по умолчанию, методы получения / установки и т. Д.), Но если нет, возникают проблемы. Вот пример класса, который проблематичен. Я написал этот класс как тестовый псевдоним для * 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, Джексон вводит информацию о его типе ("@class": "com.test.ProblematicClass"), и это вызывает проблему во втором примере ({"a": 10}). Итак, в serializeWithType я написал gen.writeFieldName("@json");, чтобы «обернуть» JSON, поэтому он должен выглядеть следующим образом (с метаинформацией Джексона):

{"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);
    }
}

Вот полный рабочий MVCE на pastebin .

Любые предложения о том, как преодолеть эту проблему? Но, пожалуйста, не предлагайте «всегда оборачивать тип в обертке», например. class ProblematicClassHolder {public ProblematicClass pclass; }.

Большое спасибо за чтение этого длинного вопроса и, конечно же, за все комментарии и ответы.

...