2016-03-22 27 views
9

Immaginate il seguente scenario:Come creare un deserializzatore personalizzato in Jackson per un tipo generico?

class <T> Foo<T> { 
    .... 
} 

class Bar { 
    Foo<Something> foo; 
} 

Voglio scrivere un costume Jackson deserializzatore per Foo. Per fare ciò (ad esempio, per deserializzare la classe Bar con la proprietà Foo<Something>), ho bisogno di conoscere il tipo concreto di Foo<T>, utilizzato in Bar, al momento della deserializzazione (ad es. Ho bisogno di sapere che T è Something in quello caso particolare).

Come si scrive un deserializzatore di questo tipo? Dovrebbe essere possibile farlo, dato che Jackson lo fa con raccolte e mappe tipizzate.

Chiarimenti:

sembra che ci siano 2 parti alla soluzione del problema:

1) Ottenere tipo dichiarato di proprietà foo all'interno Bar e l'uso che per deserializzare Foo<Somehting>

2) Scopri al momento della deserializzazione che stiamo deserializzando la proprietà foo all'interno della classe Bar per completare con successo il passaggio 1)

Come si completano 1 e 2?

+0

È possibile utilizzare la reflection per scoprire che il * dichiarato * il tipo di 'foo' è' Foo '. Questo è tutto. Non so di Jackson per dire se questo è un mezzo per la tua fine qui. ['Field # getGenericType'] (http://docs.oracle.com/javase/8/docs/api/java/lang/reflect/Field.html#getGenericType--) – Radiodef

risposta

23

È possibile implementare un numero personalizzato JsonDeserializer per il tipo generico che implementa anche ContextualDeserializer.

Per esempio, supponiamo di avere il seguente tipo di involucro semplice che contiene un valore generico:

public static class Wrapper<T> { 
    public T value; 
} 

Ora vogliamo deserializzare JSON che assomiglia a questo:

{ 
    "name": "Alice", 
    "age": 37 
} 

in un'istanza di una classe che assomiglia a questo:

public static class Person { 
    public Wrapper<String> name; 
    public Wrapper<Integer> age; 
} 

Implementazione ContextualDeserializer ci consente di creare un deserializzatore specifico per ogni campo nella classe Person, in base ai parametri di tipo generico del campo. Questo ci permette di deserializzare il nome come stringa e l'età come numero intero.

Il deserializzatore completo si presenta così:

public static class WrapperDeserializer extends JsonDeserializer<Wrapper<?>> implements ContextualDeserializer { 
    private JavaType valueType; 

    @Override 
    public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) throws JsonMappingException { 
     JavaType wrapperType = property.getType(); 
     JavaType valueType = wrapperType.containedType(0); 
     WrapperDeserializer deserializer = new WrapperDeserializer(); 
     deserializer.valueType = valueType; 
     return deserializer; 
    } 

    @Override 
    public Wrapper<?> deserialize(JsonParser parser, DeserializationContext ctxt) throws IOException { 
     Wrapper<?> wrapper = new Wrapper<>(); 
     wrapper.value = ctxt.readValue(parser, valueType); 
     return wrapper; 
    } 
} 

E 'meglio guardare createContextual prima qui, in quanto questo sarà chiamato prima da Jackson. Leggiamo il tipo di campo fuori dallo BeanProperty (ad esempio Wrapper<String>) e quindi estraiamo il primo parametro di tipo generico (ad esempio String). Creiamo quindi un nuovo deserializzatore e memorizziamo il tipo interno come valueType.

volta deserialize è chiamato in questa deserializzatore appena creato, possiamo semplicemente chiedere a Jackson per deserializzare il valore come il tipo interiore piuttosto che come l'intero tipo di involucro, e restituire un nuovo Wrapper contenente il valore deserializzato.

Per registrare questo deserializzatore personalizzato, abbiamo quindi bisogno di creare un modulo che lo contiene, e registriamo quel modulo:

SimpleModule module = new SimpleModule() 
     .addDeserializer(Wrapper.class, new WrapperDeserializer()); 

ObjectMapper objectMapper = new ObjectMapper(); 
objectMapper.registerModule(module); 

Se poi tenta di deserializzare l'esempio JSON dall'alto, possiamo vedere che funziona come previsto:

Person person = objectMapper.readValue(json, Person.class); 
System.out.println(person.name.value); // prints Alice 
System.out.println(person.age.value); // prints 37 

ci sono alcuni ulteriori dettagli su come contestuale deserializzatore lavorano nel Jackson documentation.

+1

Grazie per questo post. Incredibilmente utile per ottenere ciò che volevo. Ho apportato alcune modifiche che funzionano con i valori di root nel caso in cui qualcuno provenga da Google. https://gist.github.com/darylteo/a7be65b539c0d8d3ca0de94d96763f33 –

+1

Grazie, che ha davvero aiutato Il tipo contestuale stesso potrebbe essere generico, in tal caso la proprietà sarà nullo, è necessario creare JsonDeserializer utilizzando il ctxt.getContextualType –

1

Se il bersaglio stesso è un tipo generico, allora proprietà sarà nulla, per questo è necessario ottenere il valueTtype dal DeserializationContext:

@Override 
public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) throws JsonMappingException { 
    if (property == null) { // context is generic 
     JMapToListParser parser = new JMapToListParser(); 
     parser.valueType = ctxt.getContextualType().containedType(0); 
     return parser; 
    } else { // property is generic 
     JavaType wrapperType = property.getType(); 
     JavaType valueType = wrapperType.containedType(0); 
     JMapToListParser parser = new JMapToListParser(); 
     parser.valueType = valueType; 
     return parser; 
    } 
} 
+0

Per chiunque ottenga un NPE sulla risposta accettata, questa soluzione è probabilmente ciò di cui hai bisogno. Grazie - Non ho idea di quanto tempo mi hai appena salvato. – Martin