Десериализация в строку или объект с использованием Jackson

#java #jackson

#java #jackson

Вопрос:

У меня есть объект, который иногда выглядит следующим образом:

 {
   "foo" : "bar",
   "fuzz" : "bla"
}
  

и иногда выглядит так:

 {
   "foo" : { "value" : "bar", "baz": "asdf" },
   "fuzz" : { "thing" : "bla", "blip" : "asdf" }
}
  

эти классы будут выглядеть как:

 public class Foo {
   String value;
   String baz;
}

public class Fuzz {
   String thing;
   String blip;
}

  

где первые случаи являются сокращением для вторых. Я хотел бы всегда выполнять десериализацию во втором случае.

Далее — это довольно распространенный шаблон в нашем коде, поэтому я хотел бы иметь возможность выполнять сериализацию универсальным способом, поскольку существуют другие классы, похожие на Foo приведенные выше, которые имеют тот же шаблон использования String в качестве синтаксического сахара для более сложного объекта.

Я бы предположил, что код для его использования будет выглядеть примерно так

 
public class Thing { 
  @JsonProperty("fuzz")
  Fuzz fuzz;

  @JsonProperty("foo")
  Foo foo;
}

  

Как мне написать пользовательский десериализатор (или какой-либо другой модуль), который в общем случае обрабатывает оба случая?

Комментарии:

1. В вашем первом примере «foo» — это JsonNodeType . СТРОКА. В какое поле типа «Foo» вы бы хотели десериализовать это строковое значение? «значение» или «база»?

Ответ №1:

Чтобы сделать это универсальным, нам нужно иметь возможность указать имя, для которого мы хотели бы задать в object JSON primitive . Некоторую гибкость дает подход к аннотированию. Давайте определим простую аннотацию:

 @Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@interface JsonPrimitiveName {
    String value();
}
  

Название означает: в случае, если примитив появится при JSON использовании value() для получения имени свойства для данного примитива. Он связывается JSON primitive с POJO полем. Простой десериализатор, который обрабатывает JSON object и JSON primitive :

 class PrimitiveOrPojoJsonDeserializer extends JsonDeserializer implements ContextualDeserializer {

    private String primitiveName;
    private JavaType type;

    @Override
    public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
        JsonDeserializer<Object> deserializer = ctxt.findRootValueDeserializer(type);
        if (p.currentToken() == JsonToken.START_OBJECT) {
            return deserializer.deserialize(p, ctxt);
        } else if (p.currentToken() == JsonToken.VALUE_STRING) {
            BeanDeserializer beanDeserializer = (BeanDeserializer) deserializer;
            try {
                Object instance = beanDeserializer.getValueInstantiator().getDefaultCreator().call();
                SettableBeanProperty property = beanDeserializer.findProperty(primitiveName);
                property.deserializeAndSet(p, ctxt, instance);
                return instance;
            } catch (Exception e) {
                throw JsonMappingException.from(p, e.getMessage());
            }
        }

        return null;
    }

    @Override
    public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) {
        JsonPrimitiveName annotation = property.getAnnotation(JsonPrimitiveName.class);

        PrimitiveOrPojoJsonDeserializer deserializer = new PrimitiveOrPojoJsonDeserializer();
        deserializer.primitiveName = annotation.value();
        deserializer.type = property.getType();

        return deserializer;
    }
}
  

Теперь нам нужно аннотировать POJO поля, как показано ниже:

 class Root {

    @JsonPrimitiveName("value")
    @JsonDeserialize(using = PrimitiveOrPojoJsonDeserializer.class)
    private Foo foo;

    @JsonPrimitiveName("thing")
    @JsonDeserialize(using = PrimitiveOrPojoJsonDeserializer.class)
    private Fuzz fuzz;

    // getters, setters
}
  

Я предполагаю, что все классы являются POJO -s и следуют всем правилам — имеют getters , setters и конструктор по умолчанию. В случае, если конструктор не существует, вам нужно как-то изменить эту beanDeserializer.getValueInstantiator().getDefaultCreator().call() строку, которая соответствует вашим требованиям.

Пример приложения:

 import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.deser.BeanDeserializer;
import com.fasterxml.jackson.databind.deser.ContextualDeserializer;
import com.fasterxml.jackson.databind.deser.SettableBeanProperty;

import java.io.File;
import java.io.IOException;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

public class JsonApp {

    public static void main(String[] args) throws Exception {
        File jsonFile = new File("./resource/test.json").getAbsoluteFile();

        ObjectMapper mapper = new ObjectMapper();
        System.out.println(mapper.readValue(jsonFile, Root.class));
    }
}
  

Печатает для сокращенного JSON :

 Root{foo=Foo{value='bar', baz='null'}, fuzz=Fuzz{thing='bla', blip='null'}}
  

И для полной JSON полезной нагрузки:

 Root{foo=Foo{value='bar', baz='asdf'}, fuzz=Fuzz{thing='bla', blip='asdf'}}
  

Комментарии:

1. Это. Это. Удивительно.

2. Это здорово! В нашем случае мы требуем, чтобы конструкторы принимали параметры и комментировали их с помощью @JsonProperty вместо использования getDefaultCreator . Как я мог бы использовать это в таких обстоятельствах?

3. @Doug, в таком случае тебе нужно использовать call метод с Object[] : call(Object[] args) . Вам необходимо предоставить все требуемые параметры. Размер массива должен соответствовать определению конструктора. Если это не одно и то же для всех типов, вам необходимо предоставить дополнительные данные для десериализатора из аннотации. Но для простоты я предлагаю создать no-arg конструктор.