Джексон десериализует объект или массив
У меня есть вопрос Джексона.
Есть ли способ десериализации свойства, которое может иметь два типа, для некоторых объектов оно выглядит как
"someObj" : { "obj1" : 5, etc....}
то для других он выглядит как пустой массив, т.е.
"someObj" : []
Любая помощь приветствуется!
Спасибо!
Ответы
Ответ 1
В настоящее время Jackson не имеет встроенной конфигурации для автоматической обработки этого конкретного случая, поэтому необходима специальная обработка десериализации.
Ниже приведен пример того, как может выглядеть такая пользовательская десериализация.
import java.io.IOException;
import org.codehaus.jackson.JsonNode;
import org.codehaus.jackson.JsonParser;
import org.codehaus.jackson.JsonProcessingException;
import org.codehaus.jackson.Version;
import org.codehaus.jackson.annotate.JsonAutoDetect.Visibility;
import org.codehaus.jackson.annotate.JsonMethod;
import org.codehaus.jackson.map.DeserializationContext;
import org.codehaus.jackson.map.JsonDeserializer;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.module.SimpleModule;
public class JacksonFoo
{
public static void main(String[] args) throws Exception
{
// {"property1":{"property2":42}}
String json1 = "{\"property1\":{\"property2\":42}}";
// {"property1":[]}
String json2 = "{\"property1\":[]}";
SimpleModule module = new SimpleModule("", Version.unknownVersion());
module.addDeserializer(Thing2.class, new ArrayAsNullDeserializer());
ObjectMapper mapper = new ObjectMapper().setVisibility(JsonMethod.FIELD, Visibility.ANY).withModule(module);
Thing1 firstThing = mapper.readValue(json1, Thing1.class);
System.out.println(firstThing);
// output:
// Thing1: property1=Thing2: property2=42
Thing1 secondThing = mapper.readValue(json2, Thing1.class);
System.out.println(secondThing);
// output:
// Thing1: property1=null
}
}
class Thing1
{
Thing2 property1;
@Override
public String toString()
{
return String.format("Thing1: property1=%s", property1);
}
}
class Thing2
{
int property2;
@Override
public String toString()
{
return String.format("Thing2: property2=%d", property2);
}
}
class ArrayAsNullDeserializer extends JsonDeserializer<Thing2>
{
@Override
public Thing2 deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException
{
JsonNode node = jp.readValueAsTree();
if (node.isObject())
return new ObjectMapper().setVisibility(JsonMethod.FIELD, Visibility.ANY).readValue(node, Thing2.class);
return null;
}
}
(Вы можете использовать DeserializationConfig.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY, чтобы заставить вход всегда привязываться к коллекции, но это, вероятно, не тот подход, который я бы принял, учитывая, как проблема в настоящее время описана.)
Ответ 2
Изменить: с помощью Jackson 2.5.0 вы можете использовать DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_EMPTY_OBJECT, чтобы решить вашу проблему.
Решение Bruce имеет несколько проблем/недостатков:
- вам нужно будет дублировать этот код для каждого типа, который вам нужен для десериализации таким образом.
- ObjectMapper следует использовать повторно, поскольку он кэширует сериализаторы и десериализаторы и, таким образом, дорого их создавать. См. http://wiki.fasterxml.com/JacksonBestPracticesPerformance
- Если ваш массив содержит некоторые значения, вы, вероятно, хотите, чтобы джексон отказался от десериализации, потому что это означает, что возникла проблема, когда он закодирован, и вы должны увидеть и исправить это как можно скорее.
Вот мое "общее" решение для этой проблемы:
public abstract class EmptyArrayAsNullDeserializer<T> extends JsonDeserializer<T> {
private final Class<T> clazz;
protected EmptyArrayAsNullDeserializer(Class<T> clazz) {
this.clazz = clazz;
}
@Override
public T deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
ObjectCodec oc = jp.getCodec();
JsonNode node = oc.readTree(jp);
if (node.isArray() && !node.getElements().hasNext()) {
return null;
}
return oc.treeToValue(node, clazz);
}
}
то вам все равно нужно создать собственный десериализатор для каждого другого типа, но это намного проще записать, и вы не дублируете какую-либо логику:
public class Thing2Deserializer extends EmptyArrayAsNullDeserializer<Thing2> {
public Thing2Deserializer() {
super(Thing2.class);
}
}
то вы используете его как обычно:
@JsonDeserialize(using = Thing2Deserializer.class)
Если вы найдете способ избавиться от этого последнего шага, то есть реализовать 1 настраиваемый десериализатор для каждого типа, я все уши;)
Ответ 3
Еще один угол для решения этой проблемы более универсально для объектов, которые будут десериализованы с помощью BeanDeserializer, создав BeanDeserializerModifier
и зарегистрировав его с вашим картографом. BeanDeserializerModifier
является своего рода альтернативой подклассу BeanDeserializerFactory
, и он дает вам возможность вернуть что-то иное, чем обычный десериализатор, который будет использовать или изменить его.
Итак, сначала создайте новый JsonDeserializer
, который может принять другой десериализатор при его создании, а затем удерживается на этом сериализаторе. В методе десериализации вы можете проверить, передается ли вам JsonParser
, который в настоящее время указывает на JsonToken.START_ARRAY
. Если вы не передали JsonToken.START_ARRAY
, используйте просто десериализатор по умолчанию, который был передан этому пользовательскому десериализованному, когда он был создан.
Наконец, убедитесь, что реализовано ResolvableDeserializer
, так что десериализатор по умолчанию правильно привязан к контексту, который использует ваш пользовательский десериализатор.
class ArrayAsNullDeserialzer extends JsonDeserializer implements ResolvableDeserializer {
JsonDeserializer<?> mDefaultDeserializer;
@Override
/* Make sure the wrapped deserializer is usable in this deserializer contexts */
public void resolve(DeserializationContext ctxt) throws JsonMappingException {
((ResolvableDeserializer) mDefaultDeserializer).resolve(ctxt);
}
/* Pass in the deserializer given to you by BeanDeserializerModifier */
public ArrayAsNullDeserialzer(JsonDeserializer<?> defaultDeserializer) {
mDefaultDeserializer = defaultDeserializer;
}
@Override
public Object deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
JsonToken firstToken = jp.getCurrentToken();
if (firstToken == JsonToken.START_ARRAY) {
//Optionally, fail if this is something besides an empty array
return null;
} else {
return mDefaultDeserializer.deserialize(jp, ctxt);
}
}
}
Теперь, когда у нас есть наш общий десериализатор, создайте модификатор, который сможет его использовать. Это просто, просто реализуйте метод modifyDeserializer
в вашем BeanDeserializerModifier. Вам будет передан десериализатор, который был бы использован для десериализации bean. Он также передает вам BeanDesc, который будет десериализован, поэтому вы можете контролировать здесь, хотите ли вы обрабатывать [] как null для всех типов.
public class ArrayAsNullDeserialzerModifier extends BeanDeserializerModifier {
@Override
public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config, BeanDescription beanDesc, JsonDeserializer<?> deserializer) {
if ( true /* or check beanDesc to only do this for certain types, for example */ ) {
return new ArrayAsNullDeserializer(deserializer);
} else {
return deserializer;
}
}
}
Наконец, вам нужно зарегистрировать свой BeanDeserializerModifier с помощью ObjectMapper. Для этого создайте модуль и добавьте модификатор в настройку (например, для SimpleModules, похоже, нет крючка для этого). Вы можете больше узнать о модулях в другом месте, но вот пример, если у вас еще нет модуля для добавления:
Module m = new Module() {
@Override public String getModuleName() { return "MyMapperModule"; }
@Override public Version version() { return Version.unknownVersion(); }
@Override public void setupModule(Module.SetupContext context) {
context.addBeanDeserializerModifier(new ArrayAsNullDeserialzerModifier());
}
};