#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
конструктор.