2016-03-22 30 views
9

Wyobraźmy sobie następujący scenariusz:Jak utworzyć niestandardowy deserializator w Jackson dla ogólnego typu?

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

class Bar { 
    Foo<Something> foo; 
} 

Chcę napisać niestandardowy Jackson Deserializator dla Foo. Aby to zrobić (na przykład w celu deserializacji klasy Bar, która ma właściwość Foo<Something>), muszę znać konkretny typ Foo<T>, używany w Bar, w czasie deserializacji (np. Muszę wiedzieć, że T jest Something w tym przypadek particluar).

Jak można napisać taki deserializator? Powinno to być możliwe, ponieważ Jackson robi to z wpisanymi kolekcjami i mapami.

Wyjaśnienia:

wydaje się, że są 2 części do rozwiązania problemu:

1) uzyskać deklarowaną rodzaj nieruchomości foo wewnątrz Bar i używać, aby deserializowania Foo<Somehting>

2) Dowiedz się, kiedy deserializacja została przeprowadzona z deserializacją właściwości foo w klasie Bar w celu pomyślnego ukończenia kroku 1)

Jak wypełnić 1 i 2?

+0

Można użyć refleksji, aby dowiedzieć się, że zadeklarowane * * typ 'foo' to' Foo '. O to chodzi. Nie wiem, czy Jackson powiedział, czy to jest środek twojego celu tutaj. ['Field # getGenericType'] (http://docs.oracle.com/javase/8/docs/api/java/lang/reflect/Field.html#getGenericType--) – Radiodef

Odpowiedz

23

Możesz zaimplementować niestandardowy JsonDeserializer dla swojego rodzaju ogólnego, który również implementuje ContextualDeserializer.

Na przykład, załóżmy, że mamy następujący typ prosty wrapper, który zawiera wartość ogólna:

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

teraz chcemy deserializować JSON, który wygląda tak:

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

do instancji Klasa wyglądająca następująco:

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

Implementacja ContextualDeserializer pozwala nam utworzyć określony deserializator dla każdego pola w klasie Person, w oparciu o ogólne parametry typu pola. Dzięki temu możemy deserializować nazwę jako ciąg, a wiek jako liczbę całkowitą.

Kompletny Deserializator wygląda następująco:

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

Jest najlepiej spojrzeć na createContextual tu pierwszy, jak to będzie pierwszy powołany przez Jacksona. Odczytujemy typ pola z BeanProperty (np. Wrapper<String>), a następnie wyodrębniamy pierwszy typowy parametr (np. String). Następnie tworzymy nowy deserializer i przechowujemy wewnętrzny typ jako valueType.

Po wywołaniu deserialize na nowo utworzonym deserializatorze, możemy po prostu poprosić Jacksona o deserializację wartości jako typu wewnętrznego, a nie jako całego typu opakowania, i zwrócić nowy Wrapper zawierający deserializowaną wartość.

W celu zarejestrowania tego niestandardowego Deserializator, my wtedy trzeba utworzyć moduł, który go zawiera, i zarejestrować ten moduł:

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

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

Gdybyśmy następnie spróbuj deserializowania przykładowy JSON z góry, widzimy że działa zgodnie z oczekiwaniami:

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

Istnieje kilka szczegółów na temat jak kontekstowe deserializers pracować w Jackson documentation.

+1

Dziękuję za ten wpis. Niezwykle pomocny w osiągnięciu tego, co chciałem. Wprowadziłem kilka zmian, które działają z wartościami root, na wypadek, gdyby ktoś inny z Google się pojawił. https://gist.github.com/darylteo/a7be65b539c0d8d3ca0de94d96763f33 –

+1

Dziękujemy, że naprawdę pomogło Typ kontekstowy sam w sobie może być ogólny, w tym przypadku właściwość będzie pusta, musisz utworzyć JsonDeserializer używając ctxt.getContextualType –

1

Jeśli sam cel jest typem generycznym następnie nieruchomość będzie zerowa, za które będzie trzeba uzyskać valueTtype z 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

Dla każdy, kto otrzyma NPE po zaakceptowanej odpowiedzi, to rozwiązanie prawdopodobnie jest tym, czego potrzebujesz. Dziękuję - nie mam pojęcia, ile czasu mnie uratowałeś. – Martin