Come posso chiamare il deserializzatore predefinito da un deserializzatore personalizzato in Jackson


105

Ho un problema con il mio deserializzatore personalizzato a Jackson. Voglio accedere al serializzatore predefinito per popolare l'oggetto in cui deserializzo. Dopo il popolamento, farò alcune cose personalizzate, ma prima deserializzare l'oggetto con il comportamento Jackson predefinito.

Questo è il codice che ho al momento.

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

}

Ciò di cui ho bisogno è un modo per inizializzare il deserializzatore predefinito in modo da poter precompilare il mio POJO prima di avviare la mia logica speciale.

Quando si chiama deserialize dall'interno del deserializzatore personalizzato Sembra che il metodo venga chiamato dal contesto corrente indipendentemente da come costruisco la classe serializer. A causa dell'annotazione nel mio POJO. Ciò causa un'eccezione di overflow dello stack per ovvi motivi.

Ho provato a inizializzare a BeanDeserializerma il processo è estremamente complesso e non sono riuscito a trovare il modo giusto per farlo. Ho anche provato a sovraccaricare AnnotationIntrospectorinutilmente il file, pensando che potrebbe aiutarmi a ignorare l'annotazione nel file DeserializerContext. Alla fine sembra che avrei potuto avere un certo successo usando JsonDeserializerBuildersanche se questo mi ha richiesto di fare alcune cose magiche per ottenere il contesto dell'applicazione da Spring. Apprezzerei qualsiasi cosa che possa portarmi a una soluzione più pulita, ad esempio come posso costruire un contesto di deserializzazione senza leggere l' JsonDeserializerannotazione.


2
No. Questi approcci non aiuteranno: il problema è che avrai bisogno di un deserializzatore predefinito completamente costruito; e questo richiede che ne venga creato uno e quindi il tuo deserializzatore possa accedervi. DeserializationContextnon è qualcosa che dovresti creare o modificare; sarà fornito da ObjectMapper. AnnotationIntrospector, allo stesso modo, non sarà di aiuto per ottenere l'accesso.
StaxMan

Come hai fatto a farlo alla fine?
Khituras

Buona domanda. Non ne sono sicuro, ma sono certo che la risposta qui sotto mi abbia aiutato. Al momento non sono in possesso del codice che abbiamo scritto se trovi una soluzione per favore pubblicalo qui per altri.
Pablo Jomer

Risposte:


93

Come già suggerito da StaxMan, puoi farlo scrivendo a BeanDeserializerModifiere registrandolo tramite SimpleModule. Il seguente esempio dovrebbe funzionare:

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

Grazie! L'ho già risolto in un altro modo, ma esaminerò la tua soluzione quando avrò più tempo.
Pablo Jomer

5
C'è un modo per fare lo stesso ma con a JsonSerializer? Ho diversi serializzatori ma hanno codice in comune, quindi voglio generarlo. Provo a chiamare direttamente il serializzatore ma il risultato non viene scartato nel risultato JSON (ogni chiamata del serializzatore crea un nuovo oggetto)
herau

1
@herau BeanSerializerModifier, ResolvableSerializere ContextualSerializersono le interfacce di corrispondenza da utilizzare per la serializzazione.
StaxMan

È applicabile ai contenitori dell'edizione EE (Wildfly 10)? Ottengo JsonMappingException: (era java.lang.NullPointerException) (tramite catena di riferimenti: java.util.ArrayList [0])
user1927033

La domanda usa readTree()ma la risposta no. Qual è il vantaggio di questo approccio rispetto a quello pubblicato da Derek Cochran ? C'è un modo per farlo funzionare readTree()?
Gili

14

Ho trovato una risposta in ans che è molto più leggibile della risposta accettata.

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

Non è davvero più facile di così.


Ciao Gili! Grazie per questo, spero che le persone trovino questa risposta e abbiano il tempo di convalidarla. Non sono più in grado di farlo lì perché non posso accettare la risposta in questo momento. Se vedo che le persone dicono che questa è una possibile soluzione, ovviamente le guiderò verso di essa. Può anche darsi che ciò non sia possibile per tutte le versioni. Grazie ancora per la condivisione.
Pablo Jomer

Non si compila con Jackson 2.9.9. JsonParser.readTree () non esiste.
ccleve

@ccleve Sembra un semplice errore di battitura. Fisso.
Gili

Posso confermare che funziona con Jackson 2.10, grazie!
Stuart Leyland-Cole

3
Non capisco come StackOverflowErrorUser
funzioni

12

Il DeserializationContextha un readValue()metodo che è possibile utilizzare. Questo dovrebbe funzionare sia per il deserializzatore predefinito che per qualsiasi deserializzatore personalizzato di cui disponi.

Assicurati solo di chiamare traverse()il JsonNodelivello che vuoi leggere per recuperare il JsonParsera cui passare 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;
    }

}

DeserialisationContext.readValue () non esiste, ovvero un metodo di ObjectMapper
Pedro Borges

questa soluzione funziona bene, tuttavia potresti dover chiamare nextToken () se deserializza una classe di valore, ad esempio Date.class
revau.lt

9

Ci sono un paio di modi per farlo, ma farlo bene richiede un po 'più di lavoro. Fondamentalmente non è possibile utilizzare la sottoclasse, poiché le informazioni di cui i deserializzatori predefiniti necessitano sono costruite dalle definizioni di classe.

Quindi quello che puoi usare molto probabilmente è costruire un BeanDeserializerModifier, registrarlo tramite l' Moduleinterfaccia (usa SimpleModule). È necessario definire / sovrascrivere modifyDeserializere, per il caso specifico in cui si desidera aggiungere la propria logica (dove il tipo corrisponde), costruire il proprio deserializzatore, passare il deserializzatore predefinito che viene fornito. E poi nel deserialize()metodo puoi semplicemente delegare la chiamata, prendere il risultato Object.

In alternativa, se devi effettivamente creare e popolare l'oggetto, puoi farlo e chiamare la versione sovraccarica deserialize()che richiede il terzo argomento; oggetto in cui deserializzare.

Un altro modo che potrebbe funzionare (ma non sicuro al 100%) sarebbe specificare Converterobject ( @JsonDeserialize(converter=MyConverter.class)). Questa è una nuova funzionalità di Jackson 2.2. Nel tuo caso, Converter non converte effettivamente il tipo, ma semplifica la modifica dell'oggetto: ma non so se questo ti permetterebbe di fare esattamente quello che vuoi, poiché il deserializzatore predefinito verrebbe chiamato prima, e solo dopo il tuo Converter.


La mia risposta è ancora valida: devi lasciare che Jackson costruisca il deserializzatore predefinito a cui delegare; e devo trovare un modo per "sovrascriverlo". BeanDeserializerModifierè il gestore di callback che lo consente.
StaxMan

7

Se è possibile dichiarare una classe utente aggiuntiva, è possibile implementarla semplicemente utilizzando le annotazioni

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

}

1
Sì. L'unico approccio che ha funzionato per me. Ricevo StackOverflowErrors a causa di una chiamata ricorsiva al deserializzatore.
ccleve

3

Sulla falsariga di quanto suggerito da Tomáš Záluský , nei casi in cui l'utilizzo non BeanDeserializerModifierè desiderabile, puoi costruire tu stesso un deserializzatore predefinito BeanDeserializerFactory, sebbene sia necessaria qualche configurazione aggiuntiva. Nel contesto, questa soluzione sarebbe simile a questa:

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

Funziona come un sogno con Jackson 2.9.9. Non soffre di StackOverflowError come l'altro esempio fornito.
meta1203

2

Ecco un oneliner che utilizza 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;
}

E per favore: non c'è davvero bisogno di usare alcun valore String o qualcos'altro. Tutte le informazioni necessarie sono fornite da JsonParser, quindi usale.


1

Non mi andava bene l'uso BeanSerializerModifierpoiché costringe a dichiarare alcune modifiche comportamentali ObjectMappernel deserializzatore centrale piuttosto che nel deserializzatore personalizzato stesso e in effetti è una soluzione parallela all'annotazione della classe di entità con JsonSerialize. Se la senti allo stesso modo, potresti apprezzare la mia risposta qui: https://stackoverflow.com/a/43213463/653539


1

Una soluzione più semplice per me era semplicemente aggiungere un altro bean di ObjectMappere usarlo per deserializzare l'oggetto (grazie al commento https://stackoverflow.com/users/1032167/varren ) - nel mio caso ero interessato a deserializzare il suo id (un int) o l'intero oggetto 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;
     }
}

per ogni entità che deve passare attraverso il deserializzatore personalizzato, dobbiamo configurarlo nel ObjectMapperbean globale dell'app Spring Boot nel mio caso (ad esempio per 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;
}

0

Sei destinato a fallire se provi a creare il tuo deserializzatore personalizzato da zero.

Invece, è necessario ottenere l'istanza del deserializzatore predefinito (completamente configurato) tramite un personalizzato BeanDeserializerModifier, quindi passare questa istanza alla classe del deserializzatore personalizzato:

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

Nota: la registrazione di questo modulo sostituisce l' @JsonDeserializeannotazione, ovvero la Userclasse oi Usercampi non dovrebbero più essere annotati con questa annotazione.

Il deserializzatore personalizzato dovrebbe quindi essere basato su un in DelegatingDeserializermodo che tutti i metodi deleghino, a meno che non si fornisca un'implementazione esplicita:

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

0

L'uso BeanDeserializerModifierfunziona bene, ma se devi usarlo JsonDeserializec'è un modo per farlo in AnnotationIntrospector questo modo:

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

Ora il mappatore copiato ignorerà il deserializzatore personalizzato (MyDeserializer.class) e utilizzerà l'implementazione predefinita. Puoi usarlo all'interno del deserializemetodo del tuo deserializzatore personalizzato per evitare la ricorsione rendendo statico il mappatore copiato o cablarlo se usi Spring.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.