2011-11-29 11 views
11

Ho una domanda di Jackson.Jackson deserialize object or array

C'è un modo per deserializzare una proprietà che può avere due tipi, per alcuni oggetti emerge simili

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

poi per altri appare come un array vuoto, cioè

"someObj" : [] 

Qualsiasi aiuto è apprezzato!

Grazie!

risposta

12

Jackson non dispone attualmente di una configurazione integrata per gestire automaticamente questo caso particolare, quindi è necessario l'elaborazione di deserializzazione personalizzata.

Di seguito è riportato un esempio di come potrebbe essere la deserializzazione personalizzata.

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

(si potrebbe fare uso di DeserializationConfig.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY per forzare l'ingresso di legarsi sempre di una collezione, ma non è probabilmente l'approccio mi piacerebbe prendere dato come il problema viene attualmente descritto.)

+0

Ok! Grazie, ho pensato che sarebbe stato il caso ... non si può mai essere facile! Ti capita di sapere di un buon tutorial su come scriverne uno? – dardo

+0

nessuna possibilità di correggere l'input originale, giusto? o, nel peggiore dei casi, se è l'unico caso, che ne è di una stringa sostituita da "[]" a "{}" – stivlo

+0

Vorrei poterlo fare, è una chiamata al servizio web di cui non ho il controllo, da un campo di battaglia cattivo sito Web dell'azienda 2. – dardo

13

Modifica: dal momento che Jackson 2.5.0, è possibile utilizzare DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_EMPTY_OBJECT per risolvere il problema.

La soluzione fornisce Bruce ha alcuni problemi/svantaggi:

  • è necessario duplicare il codice per ogni tipo è necessario deserializzare in questo modo
  • ObjectMapper dovrebbe essere riutilizzato in quanto memorizza nella cache e serializzatori deserializzatori e, quindi, è costoso da creare. Vedi http://wiki.fasterxml.com/JacksonBestPracticesPerformance
  • se il tuo array contiene alcuni valori, probabilmente lasci che deserializzi il jackson perché vuol dire che c'era un problema quando è stato codificato e dovresti vedere e risolvere il problema al più presto.

Ecco la mia soluzione "generico" per quel 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); 
    } 
} 

allora ancora bisogno di creare un deserializzatore personalizzata per ogni tipo diverso, ma questo è molto più facile da scrivere e voi no duplicare qualsiasi logica:

public class Thing2Deserializer extends EmptyArrayAsNullDeserializer<Thing2> { 

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

allora lo si utilizza come al solito:

@JsonDeserialize(using = Thing2Deserializer.class) 

Se si trova un modo per sbarazzarsi di questo ultimo passo, cioè attuazione 1 personalizzato deserializzatore in base alla tipologia, sono tutto orecchie;)

+0

Questa soluzione appare come quello che ho bisogno .. grazie .. comunque Jackson è gettando un JsonMappingException: non ha alcun difetto (ARG) costruttore quando si tenta di usalo Ho provato ad aggiungere un costruttore predefinito alla classe base, ma non sto avendo fortuna. – speedynomads

+0

Assicurati che la classe che passi a Jackson per deserializzare abbia un costruttore pubblico predefinito (non solo la sua classe base). Nell'esempio sopra, ho deserializzato con Thing2.class; La classe di Thing2 ha un costruttore pubblico predefinito. Assicurarsi che tutte le sottoclassi della classe che si sta tentando di deserializzare abbiano anche un costruttore pubblico predefinito. Nel dubbio, prova con una classe più semplice con solo tipi primitivi e se funziona sai che è un problema con una delle tue classi;) – fabien

2

C'è un altro angolo per affrontare questo problema più genericamente per gli oggetti che sarebbero deserializzato usando il BeanDeserializer, creando un BeanDeserializerModifier e registrandolo con il tuo mapper. BeanDeserializerModifier è una sorta di alternativa alla sottoclasse BeanDeserializerFactory e ti dà la possibilità di restituire qualcosa di diverso dal normale deserializzatore che verrebbe utilizzato, o di modificarlo.

Quindi, prima creare un nuovo JsonDeserializer che può accettare un altro deserializzatore durante la sua costruzione e quindi conservare su quel serializzatore. Nel metodo deserialize, puoi verificare se ti viene passato un JsonParser che punta attualmente a un JsonToken.START_ARRAY. Se non si è passati a JsonToken.START_ARRAY, è sufficiente utilizzare il deserializzatore predefinito passato a questa deserializzazione personalizzata al momento della creazione.

Infine, assicurarsi di implementare ResolvableDeserializer, in modo che il deserializzatore predefinito sia correttamente collegato al contesto utilizzato dall'utente deserializzatore personalizzato.

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

Ora che abbiamo il nostro gancio deserializzatore generici, creiamo un modificatore che può usarlo. Questo è facile, basta implementare il metodo modifyDeserializer nel BeanDeserializerModifier. Verrà passato il deserializzatore che sarebbe stato utilizzato per deserializzare il bean. Ti passa anche il BeanDesc che verrà deserializzato, quindi puoi controllare qui se vuoi o meno gestire [] come null per tutti i tipi.

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

Infine, è necessario registrare il BeanDeserializerModifier con l'ObjectMapper. Per fare questo, crea un modulo e aggiungi il modificatore nel setup (i SimpleModules non sembrano avere un aggancio per questo, sfortunatamente). Puoi leggere ulteriori informazioni sui moduli altrove, ma ecco un esempio se non hai già un modulo da aggiungere 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()); 
    } 
};