Jak wywołać domyślny deserializator z niestandardowego deserializatora w Jackson

105

Mam problem z moim niestandardowym deserializatorem w Jackson. Chcę uzyskać dostęp do domyślnego serializatora, aby wypełnić obiekt, do którego deserializuję. Po populacji zrobię kilka niestandardowych rzeczy, ale najpierw chcę deserializować obiekt z domyślnym zachowaniem Jacksona.

To jest kod, który mam w tej chwili.

public class UserEventDeserializer extends StdDeserializer<User> {

  private static final long serialVersionUID = 7923585097068641765L;

  public UserEventDeserializer() {
    super(User.class);
  }

  @Override
  @Transactional
  public User deserialize(JsonParser jp, DeserializationContext ctxt)
      throws IOException, JsonProcessingException {

    ObjectCodec oc = jp.getCodec();
    JsonNode node = oc.readTree(jp);
    User deserializedUser = null;
    deserializedUser = super.deserialize(jp, ctxt, new User()); 
    // The previous line generates an exception java.lang.UnsupportedOperationException
    // Because there is no implementation of the deserializer.
    // I want a way to access the default spring deserializer for my User class.
    // How can I do that?

    //Special logic

    return deserializedUser;
  }

}

To, czego potrzebuję, to sposób na zainicjowanie domyślnego deserializatora, aby móc wstępnie wypełnić moje POJO przed rozpoczęciem mojej specjalnej logiki.

Podczas wywoływania deserializacji z poziomu niestandardowego deserializatora Wygląda na to, że metoda jest wywoływana z bieżącego kontekstu, niezależnie od tego, jak skonstruuję klasę serializatora. Z powodu adnotacji w moim POJO. Powoduje to wyjątek przepełnienia stosu z oczywistych powodów.

Próbowałem zainicjować plik, BeanDeserializerale proces jest niezwykle złożony i nie udało mi się znaleźć właściwego sposobu, aby to zrobić. Próbowałem również przeciążać AnnotationIntrospectorplik bez skutku, myśląc, że może mi to pomóc zignorować adnotację w DeserializerContext. W końcu wydaje się, że mogłem odnieść sukces, JsonDeserializerBuilderschociaż wymagało to ode mnie magicznych rzeczy, aby poznać kontekst aplikacji ze Springa. Byłbym wdzięczny za każdą rzecz, która mogłaby doprowadzić mnie do bardziej przejrzystego rozwiązania, na przykład jak mogę skonstruować kontekst deserializacji bez czytania JsonDeserializeradnotacji.

Pablo Jomer
źródło
2
Nie. Te podejścia nie pomogą: problem polega na tym, że będziesz potrzebować w pełni skonstruowanego domyślnego deserializatora; a to wymaga, aby jeden został skompilowany, a następnie twój deserializator uzyska do niego dostęp. DeserializationContextnie jest czymś, co powinieneś stworzyć lub zmienić; będzie dostarczony przez ObjectMapper. AnnotationIntrospectorpodobnie nie pomoże w uzyskaniu dostępu.
StaxMan
Jak ostatecznie to zrobiłeś?
khituras
Dobre pytanie. Nie jestem pewien, ale jestem pewien, że poniższa odpowiedź pomogła mi. Obecnie nie posiadam kodu, który napisaliśmy, jeśli znajdziesz rozwiązanie, prześlij je tutaj innym.
Pablo Jomer

Odpowiedzi:

93

Jak już zasugerował StaxMan, możesz to zrobić, pisząc BeanDeserializerModifieri rejestrując go przez SimpleModule. Poniższy przykład powinien działać:

public class UserEventDeserializer extends StdDeserializer<User> implements ResolvableDeserializer
{
  private static final long serialVersionUID = 7923585097068641765L;

  private final JsonDeserializer<?> defaultDeserializer;

  public UserEventDeserializer(JsonDeserializer<?> defaultDeserializer)
  {
    super(User.class);
    this.defaultDeserializer = defaultDeserializer;
  }

  @Override public User deserialize(JsonParser jp, DeserializationContext ctxt)
      throws IOException, JsonProcessingException
  {
    User deserializedUser = (User) defaultDeserializer.deserialize(jp, ctxt);

    // Special logic

    return deserializedUser;
  }

  // for some reason you have to implement ResolvableDeserializer when modifying BeanDeserializer
  // otherwise deserializing throws JsonMappingException??
  @Override public void resolve(DeserializationContext ctxt) throws JsonMappingException
  {
    ((ResolvableDeserializer) defaultDeserializer).resolve(ctxt);
  }


  public static void main(String[] args) throws JsonParseException, JsonMappingException, IOException
  {
    SimpleModule module = new SimpleModule();
    module.setDeserializerModifier(new BeanDeserializerModifier()
    {
      @Override public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config, BeanDescription beanDesc, JsonDeserializer<?> deserializer)
      {
        if (beanDesc.getBeanClass() == User.class)
          return new UserEventDeserializer(deserializer);
        return deserializer;
      }
    });


    ObjectMapper mapper = new ObjectMapper();
    mapper.registerModule(module);
    User user = mapper.readValue(new File("test.json"), User.class);
  }
}
schummar
źródło
Dzięki! Rozwiązałem to już w inny sposób, ale przyjrzę się twojemu rozwiązaniu, gdy będę miał więcej czasu.
Pablo Jomer
5
Czy jest sposób, aby zrobić to samo, ale za pomocą JsonSerializer? Mam kilka serializatorów, ale mają wspólny kod, więc chcę go wygenerować. Próbuję bezpośrednio wywołać serializator, ale wynik nie jest rozpakowany w wyniku JSON (każde wywołanie serializatora tworzy nowy obiekt)
herau
1
@herau BeanSerializerModifier, ResolvableSerializeri ContextualSerializersą dopasowane do wykorzystania interfejsy dla serializacji.
StaxMan
Czy dotyczy to kontenerów w wersji EE (Wildfly 10)? Otrzymuję JsonMappingException: (był java.lang.NullPointerException) (przez łańcuch referencji: java.util.ArrayList [0])
user1927033
Pytanie używa, readTree()ale odpowiedź nie. Jaka jest przewaga tego podejścia w porównaniu z podejściem opublikowanym przez Dereka Cochrana ? Czy jest sposób, aby to zadziałało readTree()?
Gili
14

Znalazłem odpowiedź w ans, która jest znacznie bardziej czytelna niż zaakceptowana odpowiedź.

    public User deserialize(JsonParser jp, DeserializationContext ctxt)
        throws IOException, JsonProcessingException {
            User user = jp.readValueAs(User.class);
             // some code
             return user;
          }

To naprawdę nie jest łatwiejsze niż to.

Gili
źródło
Cześć Gili! Dzięki za to mam nadzieję, że ludzie znajdą tę odpowiedź i będą mieli czas, aby ją potwierdzić. Nie jestem już w stanie tego zrobić, ponieważ w tej chwili nie mogę przyjąć odpowiedzi. Jeśli zobaczę, że ludzie mówią, że jest to możliwe rozwiązanie, oczywiście poprowadzę ich w tym kierunku. Może się również zdarzyć, że nie jest to możliwe we wszystkich wersjach. Wciąż dziękuję za udostępnienie.
Pablo Jomer
Nie kompiluje się z Jacksonem 2.9.9. JsonParser.readTree () nie istnieje.
ccleve
@ccleve Wygląda na zwykłą literówkę. Naprawiony.
Gili
Potwierdzam, że to działa z Jacksonem 2.10, dzięki!
Stuart Leyland-Cole
3
Nie rozumiem, jak to działa, skutkuje to StackOverflowError, ponieważ Jackson ponownie użyje tego samego serializatora dla User...
john16384
12

DeserializationContextMa readValue()metodę można wykorzystać. Powinno to działać zarówno w przypadku domyślnego deserializatora, jak i wszystkich posiadanych niestandardowych deserializatorów.

Tylko pamiętaj, aby zadzwonić traverse()na JsonNodepoziomie, który chcesz przeczytać, aby odzyskać JsonParserprzejść do readValue().

public class FooDeserializer extends StdDeserializer<FooBean> {

    private static final long serialVersionUID = 1L;

    public FooDeserializer() {
        this(null);
    }

    public FooDeserializer(Class<FooBean> t) {
        super(t);
    }

    @Override
    public FooBean deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
        JsonNode node = jp.getCodec().readTree(jp);
        FooBean foo = new FooBean();
        foo.setBar(ctxt.readValue(node.get("bar").traverse(), BarBean.class));
        return foo;
    }

}
Derek Cochran
źródło
DeserialisationContext.readValue () nie istnieje, to jest metoda ObjectMapper
Pedro Borges
to rozwiązanie działa dobrze, jednak może być konieczne wywołanie nextToken (), jeśli deserializujesz klasę wartości, np. Date.class
revau.lt
9

Jest na to kilka sposobów, ale zrobienie tego dobrze wymaga trochę więcej pracy. Zasadniczo nie można używać podklas, ponieważ informacje, których potrzebują domyślne deserializatory, są budowane na podstawie definicji klas.

Więc to, co najprawdopodobniej możesz użyć, to skonstruować BeanDeserializerModifier, zarejestrować to przez Moduleinterfejs (użyj SimpleModule). Musisz zdefiniować / przesłonić modifyDeserializer, a dla konkretnego przypadku, w którym chcesz dodać własną logikę (gdzie typ jest zgodny), skonstruować własny deserializator, przekazać domyślny deserializator, który otrzymałeś. A następnie w deserialize()metodzie możesz po prostu delegować wywołanie, przyjmować wynik Object.

Alternatywnie, jeśli musisz faktycznie utworzyć i zapełnić obiekt, możesz to zrobić i wywołać przeciążoną wersję tego, deserialize()która przyjmuje trzeci argument; obiekt do deserializacji.

Innym sposobem, który może zadziałać (ale nie na 100%), byłoby określenie Converterobiektu ( @JsonDeserialize(converter=MyConverter.class)). To jest nowa funkcja Jacksona 2.2. W twoim przypadku Converter faktycznie nie konwertuje typu, ale upraszcza modyfikację obiektu: ale nie wiem, czy pozwoliłoby ci to zrobić dokładnie to, co chcesz, ponieważ domyślny deserializator zostałby wywołany jako pierwszy, a dopiero potem twój Converter.

StaxMan
źródło
Moja odpowiedź jest nadal aktualna: musisz pozwolić Jacksonowi na skonstruowanie domyślnego deserializatora, któremu ma dokonać delegacji; i muszę znaleźć sposób, aby to „zastąpić”. BeanDeserializerModifierto program obsługi wywołań zwrotnych, który na to pozwala.
StaxMan
7

Jeśli możesz zadeklarować dodatkową klasę użytkownika, możesz ją zaimplementować po prostu za pomocą adnotacji

// your class
@JsonDeserialize(using = UserEventDeserializer.class)
public class User {
...
}

// extra user class
// reset deserializer attribute to default
@JsonDeserialize
public class UserPOJO extends User {
}

public class UserEventDeserializer extends StdDeserializer<User> {

  ...
  @Override
  public User deserialize(JsonParser jp, DeserializationContext ctxt)
      throws IOException, JsonProcessingException {
    // specify UserPOJO.class to invoke default deserializer
    User deserializedUser = jp.ReadValueAs(UserPOJO.class);
    return deserializedUser;

    // or if you need to walk the JSON tree

    ObjectMapper mapper = (ObjectMapper) jp.getCodec();
    JsonNode node = oc.readTree(jp);
    // specify UserPOJO.class to invoke default deserializer
    User deserializedUser = mapper.treeToValue(node, UserPOJO.class);

    return deserializedUser;
  }

}
Rachunek
źródło
1
Tak. Jedyne podejście, które zadziałało dla mnie. Otrzymałem StackOverflowErrors z powodu rekurencyjnego wywołania deserializatora.
ccleve
3

Zgodnie z tym, co zasugerował Tomáš Záluský , w przypadkach, gdy używanie BeanDeserializerModifierjest niepożądane, możesz samodzielnie skonstruować domyślny deserializator BeanDeserializerFactory, chociaż jest wymagana dodatkowa konfiguracja. W kontekście takie rozwiązanie wyglądałoby tak:

public User deserialize(JsonParser jp, DeserializationContext ctxt)
  throws IOException, JsonProcessingException {

    ObjectCodec oc = jp.getCodec();
    JsonNode node = oc.readTree(jp);
    User deserializedUser = null;

    DeserializationConfig config = ctxt.getConfig();
    JavaType type = TypeFactory.defaultInstance().constructType(User.class);
    JsonDeserializer<Object> defaultDeserializer = BeanDeserializerFactory.instance.buildBeanDeserializer(ctxt, type, config.introspect(type));

    if (defaultDeserializer instanceof ResolvableDeserializer) {
        ((ResolvableDeserializer) defaultDeserializer).resolve(ctxt);
    }

    JsonParser treeParser = oc.treeAsTokens(node);
    config.initialize(treeParser);

    if (treeParser.getCurrentToken() == null) {
        treeParser.nextToken();
    }

    deserializedUser = (User) defaultDeserializer.deserialize(treeParser, context);

    return deserializedUser;
}
zbiornik
źródło
To działa jak sen z Jacksonem 2.9.9. Nie cierpi z powodu StackOverflowError, jak w innym podanym przykładzie.
meta1203
2

Oto oneliner używający ObjectMapper

public MyObject deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
    OMyObject object = new ObjectMapper().readValue(p, MyObject.class);
    // do whatever you want 
    return object;
}

I proszę: naprawdę nie ma potrzeby używania żadnej wartości String ani czegoś innego. Wszystkie potrzebne informacje są dostarczane przez JsonParser, więc używaj go.

kajzer
źródło
1

Nie byłem w porządku z używaniem, BeanSerializerModifierponieważ wymusza to deklarowanie pewnych zmian behawioralnych w centralnym, ObjectMappera nie w niestandardowym deserializatorze i w rzeczywistości jest to równoległe rozwiązanie do dodawania adnotacji do klasy encji za pomocą JsonSerialize. Jeśli czujesz to podobnie, możesz docenić moją odpowiedź tutaj: https://stackoverflow.com/a/43213463/653539

Tomáš Záluský
źródło
1

Prostszym rozwiązaniem było dla mnie po prostu dodanie kolejnego beana ObjectMapperi użycie go do deserializacji obiektu (dzięki komentarzowi https://stackoverflow.com/users/1032167/varren ) - w moim przypadku byłem zainteresowany albo deserializacją do jego id (int) lub cały obiekt https://stackoverflow.com/a/46618193/986160

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import org.springframework.context.annotation.Bean;

import java.io.IOException;

public class IdWrapperDeserializer<T> extends StdDeserializer<T> {

    private Class<T> clazz;

    public IdWrapperDeserializer(Class<T> clazz) {
        super(clazz);
        this.clazz = clazz;
    }

    @Bean
    public ObjectMapper objectMapper() {
        ObjectMapper mapper = new ObjectMapper();
        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        mapper.configure(MapperFeature.DEFAULT_VIEW_INCLUSION, true);
        mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE);
        mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
        return mapper;
    }

    @Override
    public T deserialize(JsonParser jp, DeserializationContext dc) throws IOException, JsonProcessingException {
        String json = jp.readValueAsTree().toString();
          // do your custom deserialization here using json
          // and decide when to use default deserialization using local objectMapper:
          T obj = objectMapper().readValue(json, clazz);

          return obj;
     }
}

dla każdego podmiotu, który musi przejść przez niestandardowy deserializer, musimy skonfigurować go w globalnym ObjectMapperbeanie aplikacji Spring Boot w moim przypadku (np. dla Category):

@Bean
public ObjectMapper objectMapper() {
    ObjectMapper mapper = new ObjectMapper();
                mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
            mapper.configure(MapperFeature.DEFAULT_VIEW_INCLUSION, true);
            mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
            mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE);
            mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
    SimpleModule testModule = new SimpleModule("MyModule")
            .addDeserializer(Category.class, new IdWrapperDeserializer(Category.class))

    mapper.registerModule(testModule);

    return mapper;
}
Michail Michailidis
źródło
0

Jeśli spróbujesz utworzyć niestandardowy deserializator od podstaw, poniesiesz porażkę.

Zamiast tego musisz uzyskać (w pełni skonfigurowane) domyślne wystąpienie deserializatora za pośrednictwem niestandardowego BeanDeserializerModifier, a następnie przekazać to wystąpienie do niestandardowej klasy deserializatora:

public ObjectMapper getMapperWithCustomDeserializer() {
    ObjectMapper objectMapper = new ObjectMapper();

    SimpleModule module = new SimpleModule();
    module.setDeserializerModifier(new BeanDeserializerModifier() {
        @Override
        public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config,
                    BeanDescription beanDesc, JsonDeserializer<?> defaultDeserializer) 
            if (beanDesc.getBeanClass() == User.class) {
                return new UserEventDeserializer(defaultDeserializer);
            } else {
                return defaultDeserializer;
            }
        }
    });
    objectMapper.registerModule(module);

    return objectMapper;
}

Uwaga: Ta rejestracja modułu zastępuje @JsonDeserializeadnotację, tj. UserKlasa lub Userpola nie powinny już być adnotowane tą adnotacją.

Niestandardowy deserializator powinien następnie być oparty na DelegatingDeserializertak, aby delegować wszystkie metody, chyba że podasz jawną implementację:

public class UserEventDeserializer extends DelegatingDeserializer {

    public UserEventDeserializer(JsonDeserializer<?> delegate) {
        super(delegate);
    }

    @Override
    protected JsonDeserializer<?> newDelegatingInstance(JsonDeserializer<?> newDelegate) {
        return new UserEventDeserializer(newDelegate);
    }

    @Override
    public User deserialize(JsonParser p, DeserializationContext ctxt)
            throws IOException {
        User result = (User) super.deserialize(p, ctxt);

        // add special logic here

        return result;
    }
}
oberlies
źródło
0

Używanie BeanDeserializerModifierdziała dobrze, ale jeśli musisz go użyć JsonDeserialize, jest na to sposób AnnotationIntrospector :

ObjectMapper originalMapper = new ObjectMapper();
ObjectMapper copy = originalMapper.copy();//to keep original configuration
copy.setAnnotationIntrospector(new JacksonAnnotationIntrospector() {

            @Override
            public Object findDeserializer(Annotated a) {
                Object deserializer = super.findDeserializer(a);
                if (deserializer == null) {
                    return null;
                }
                if (deserializer.equals(MyDeserializer.class)) {
                    return null;
                }
                return deserializer;
            }
});

Teraz skopiowany program mapujący będzie teraz ignorował Twój niestandardowy deserializer (MyDeserializer.class) i użyje domyślnej implementacji. Możesz go użyć wewnątrz deserializemetody niestandardowego deserializatora, aby uniknąć rekursji, ustawiając skopiowany element mapper jako statyczny lub łącząc go, jeśli używasz Spring.

Filip Ljubičić
źródło