2011-11-29 17 views
11

Tengo una pregunta de Jackson.Jackson deserializar objeto o matriz

¿Hay una manera de deserializar una propiedad que puede tener dos tipos, para algunos objetos que aparecen de esta forma

"someObj" : { "obj1" : 5, etc....} 

a continuación para otros aparece como una matriz vacía, es decir

"someObj" : [] 

¡Cualquier ayuda es apreciada!

Gracias!

Respuesta

12

Jackson no tiene actualmente una configuración incorporada para manejar automáticamente este caso particular, por lo que es necesario un proceso de deserialización personalizado.

A continuación se muestra un ejemplo de cómo podría ser la deserialización personalizada.

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; 
    } 
} 

(usted podría hacer uso de DeserializationConfig.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY para forzar la entrada en vincular siempre a una colección, pero eso no es probablemente el enfoque dado que tomaría forma en que el problema se describe actualmente.)

+0

Ok! Gracias, me imaginé que este sería el caso ... ¡nunca será fácil! ¿Conoces un buen tutorial sobre cómo escribir uno? – dardo

+0

no hay oportunidad de arreglar la entrada original, ¿verdad? o, en el peor de los casos, si ese es el único caso, ¿qué pasa con una cadena reemplazar "[]" por "{}" – stivlo

+0

Ojalá pudiera, es un servicio web que no tengo control, desde un campo de batalla malo sitio web de la compañía 2. – dardo

13

Editar: Desde Jackson 2.5.0, puede utilizar DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_EMPTY_OBJECT para resolver su problema.

La solución proporciona Bruce tiene unos pocos problemas/inconvenientes:

  • que necesita para duplicar ese código para cada tipo que necesita para deserializar de esa manera
  • ObjectMapper debe ser reutilizado ya que almacena en caché y serializadores deserializadores y, por lo tanto, es costoso de crear. Consulte http://wiki.fasterxml.com/JacksonBestPracticesPerformance
  • si su matriz contiene algunos valores, es probable que desee que jackson falle al deserializarlo porque significa que hubo un problema cuando se codificó y debe verlo y solucionarlo lo antes posible.

Aquí está mi solución "genérica" ​​para ese problema:

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

continuación, usted todavía tiene que crear un deserializer a medida para cada tipo diferente, pero eso es mucho más fácil de escribir y que no lo hacen duplicar cualquier lógica:

public class Thing2Deserializer extends EmptyArrayAsNullDeserializer<Thing2> { 

    public Thing2Deserializer() { 
    super(Thing2.class); 
    } 
} 

entonces que lo utilice como de costumbre:

@JsonDeserialize(using = Thing2Deserializer.class) 

Si encuentra una manera de deshacerse de ese último paso, es decir implementación de 1 personalizada deserializer según el tipo, soy todo oídos;)

+0

Esta solución se ve como lo que necesito ... gracias ... sin embargo, jackson está lanzando una JsonMappingException: no tiene un constructor predeterminado (no arg) cuando intento usarlo He intentado agregar un constructor predeterminado a la clase base pero no estoy teniendo suerte. – speedynomads

+0

Asegúrate de que la clase que pasas a jackson para deserializar tiene un constructor predeterminado público (no solo su clase base). En el ejemplo anterior, me deserializo con Thing2.class; La clase de Thing2 tiene un constructor predeterminado público. Asegúrate de que todas las subclases de la clase que intentas deserializar también tengan un constructor predeterminado público. En caso de duda, intente con una clase más simple con solo tipos primitivos y, si funciona, sabe que es un problema con una de sus clases;) – fabien

2

Hay otro punto de vista para abordar este problema de manera más genérica para los objetos que serían deserializado usando BeanDeserializer, creando un BeanDeserializerModifier y registrándolo con su asignador. BeanDeserializerModifier es una especie de alternativa a la subclase BeanDeserializerFactory, y le ofrece la posibilidad de devolver algo que no sea el deserializador normal que se usaría o de modificarlo.

Por lo tanto, primero cree un nuevo JsonDeserializer que pueda aceptar otro deserializador cuando se está construyendo, y luego se aferra a ese serializador. En el método de deserialización, puede verificar si se le está pasando un JsonParser que apunta actualmente a JsonToken.START_ARRAY. Si no pasaste el JsonToken.START_ARRAY, simplemente usa el deserializador predeterminado que se pasó a esta deserialización personalizada cuando se creó.

Finalmente, asegúrese de implementar ResolvableDeserializer, para que el deserializador predeterminado esté correctamente conectado al contexto que está utilizando su deserializador personalizado.

class ArrayAsNullDeserialzer extends JsonDeserializer implements ResolvableDeserializer { 
    JsonDeserializer<?> mDefaultDeserializer; 

    @Override 
    /* Make sure the wrapped deserializer is usable in this deserializer's 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); 
     } 
    } 
} 

Ahora que tenemos nuestro gancho deserializador genérico, creemos un modificador que pueda usarlo. Esto es fácil, simplemente implemente el método modifyDeserializer en su BeanDeserializerModifier. Se le pasará el deserializador que sería se han utilizado para deserializar el frijol. También le pasa el BeanDesc que se deserializará, por lo que puede controlar aquí si desea o no manejar [] como nulo para todos los tipos.

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; 
     } 
    } 
} 

Finalmente, tendrá que registrar su BeanDeserializerModifier con su ObjectMapper. Para hacer esto, crea un módulo y agrega el modificador en la configuración (SimpleModules no parece tener un gancho para esto, desafortunadamente). Puede leer más sobre los módulos en otro lugar, pero aquí hay un ejemplo si aún no tiene un módulo para agregar a:

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()); 
    } 
}; 
Cuestiones relacionadas