Comment appeler le désérialiseur par défaut à partir d'un désérialiseur personnalisé dans Jackson

105

J'ai un problème dans mon désérialiseur personnalisé à Jackson. Je souhaite accéder au sérialiseur par défaut pour remplir l'objet dans lequel je désérialise. Après la population, je vais faire des choses personnalisées mais je veux d'abord désérialiser l'objet avec le comportement par défaut de Jackson.

C'est le code que j'ai en ce moment.

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

}

Ce dont j'ai besoin, c'est d'un moyen d'initialiser le désérialiseur par défaut afin que je puisse pré-remplir mon POJO avant de démarrer ma logique spéciale.

Lors de l'appel de désérialisation depuis le désérialiseur personnalisé Il semble que la méthode soit appelée à partir du contexte actuel, quelle que soit la manière dont je construis la classe de sérialiseur. À cause de l'annotation dans mon POJO. Cela provoque une exception de débordement de pile pour des raisons évidentes.

J'ai essayé d'initialiser un BeanDeserializermais le processus est extrêmement complexe et je n'ai pas réussi à trouver la bonne façon de le faire. J'ai également essayé de surcharger le AnnotationIntrospectoren vain, pensant que cela pourrait m'aider à ignorer l'annotation dans le DeserializerContext. Enfin, il semble que j'aie eu un certain succès en utilisant, JsonDeserializerBuildersbien que cela m'oblige à faire des choses magiques pour obtenir le contexte de l'application de Spring. J'apprécierais toute chose qui pourrait me conduire à une solution plus propre, par exemple comment puis-je construire un contexte de désérialisation sans lire l' JsonDeserializerannotation.

Pablo Jomer
la source
2
Non. Ces approches n'aideront pas: le problème est que vous aurez besoin d'un désérialiseur par défaut entièrement construit; et cela nécessite que l'on soit construit, puis votre désérialiseur y accède. DeserializationContextn'est pas quelque chose que vous devez créer ou modifier; il sera fourni par ObjectMapper. AnnotationIntrospector, de même, ne sera d'aucune utilité pour y accéder.
StaxMan
Comment avez-vous fini par le faire?
khituras
Bonne question. Je ne suis pas sûr mais je suis certain que la réponse ci-dessous m'a aidé. Je ne suis actuellement pas en possession du code que nous avons écrit si vous trouvez une solution, veuillez le poster ici pour les autres.
Pablo Jomer

Réponses:

93

Comme StaxMan l'a déjà suggéré, vous pouvez le faire en écrivant un BeanDeserializerModifier et en l'enregistrant via SimpleModule. L'exemple suivant devrait fonctionner:

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
la source
Merci! J'ai déjà résolu cela d'une autre manière mais j'examinerai votre solution quand j'aurai plus de temps.
Pablo Jomer
5
Existe-t-il un moyen de faire la même chose mais avec un JsonSerializer? J'ai plusieurs sérialiseurs mais ils ont du code en commun donc je veux le générer. J'essaie d'appeler directement le sérialiseur mais le résultat n'est pas déroulé dans le résultat JSON (chaque appel du sérialiseur crée un nouvel objet)
herau
1
@herau BeanSerializerModifier, ResolvableSerializeret ContextualSerializerles interfaces correspondants à utiliser pour la sérialisation.
StaxMan
Cela s'applique-t-il aux conteneurs d'édition EE (Wildfly 10)? J'obtiens JsonMappingException: (était java.lang.NullPointerException) (via la chaîne de référence: java.util.ArrayList [0])
user1927033
La question utilise readTree()mais la réponse ne le fait pas. Quel est l'avantage de cette approche par rapport à celle proposée par Derek Cochran ? Y a-t-il un moyen de faire fonctionner cela readTree()?
Gili
14

J'ai trouvé une réponse à ans qui est beaucoup plus lisible que la réponse acceptée.

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

Ce n'est vraiment pas plus facile que ça.

Gili
la source
Salut Gili! Merci pour cela J'espère que les gens trouveront cette réponse et auront le temps de la valider. Je ne suis plus en mesure de le faire ici car je ne peux pas accepter la réponse pour le moment. Si je vois que les gens disent que c'est une solution possible, je les guiderai bien sûr vers elle. Il se peut également que ce ne soit pas possible pour toutes les versions. Merci encore pour le partage.
Pablo Jomer
Ne compile pas avec Jackson 2.9.9. JsonParser.readTree () n'existe pas.
ccleve
@ccleve Ressemble à une simple faute de frappe. Fixé.
Gili
Peut confirmer que cela fonctionne avec Jackson 2.10, merci!
Stuart Leyland-Cole
3
Je ne comprends pas comment cela fonctionne, cela se traduit par un StackOverflowError, puisque Jackson utilisera à nouveau le même sérialiseur pour User...
john16384
12

Le DeserializationContexta une readValue()méthode que vous pouvez utiliser. Cela devrait fonctionner à la fois pour le désérialiseur par défaut et pour tous les désérialiseurs personnalisés que vous avez.

Assurez-vous simplement d'appeler traverse()au JsonNodeniveau que vous souhaitez lire pour récupérer le JsonParserà passer 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
la source
DeserialisationContext.readValue () n'existe pas, c'est une méthode d'ObjectMapper
Pedro Borges
cette solution fonctionne bien, mais vous devrez peut-être appeler nextToken () si vous désérialisez une classe de valeur, par exemple Date.class
revau.lt
9

Il existe plusieurs façons de le faire, mais le faire correctement implique un peu plus de travail. Fondamentalement, vous ne pouvez pas utiliser de sous-classification, car les informations dont les désérialiseurs par défaut ont besoin sont construites à partir de définitions de classe.

Donc, ce que vous pouvez probablement utiliser est de construire un BeanDeserializerModifier, enregistrez-le via l' Moduleinterface (use SimpleModule). Vous devez définir / remplacermodifyDeserializer , et pour le cas spécifique où vous souhaitez ajouter votre propre logique (où le type correspond), construisez votre propre désérialiseur, passez le désérialiseur par défaut qui vous est donné. Et puis dansdeserialize() méthode, vous pouvez simplement déléguer l'appel, prendre le résultat Object.

Sinon, si vous devez réellement créer et remplir l'objet, vous pouvez le faire et appeler la version surchargée de deserialize() qui prend le troisième argument; objet dans lequel désérialiser.

Une autre façon de fonctionner (mais pas sûre à 100%) serait de spécifier Converterobject ( @JsonDeserialize(converter=MyConverter.class)). Il s'agit d'une nouvelle fonctionnalité de Jackson 2.2. Dans votre cas, Converter ne convertirait pas réellement le type, mais simplifierait la modification de l'objet: mais je ne sais pas si cela vous permettrait de faire exactement ce que vous voulez, car le désérialiseur par défaut serait appelé en premier, et seulement ensuite votre Converter.

StaxMan
la source
Ma réponse tient toujours: vous devez laisser Jackson construire le désérialiseur par défaut auquel déléguer; et doivent trouver un moyen de «passer outre». BeanDeserializerModifierest le gestionnaire de rappel qui permet cela.
StaxMan
7

S'il vous est possible de déclarer une classe utilisateur supplémentaire, vous pouvez l'implémenter simplement en utilisant des annotations

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

}
Facture
la source
1
Ouaip. La seule approche qui a fonctionné pour moi. J'obtenais StackOverflowErrors à cause d'un appel récursif au désérialiseur.
ccleve
3

Dans le sens de ce que Tomáš Záluský a suggéré , dans les cas où l'utilisation BeanDeserializerModifiern'est pas souhaitable, vous pouvez construire vous-même un désérialiseur par défaut BeanDeserializerFactory, bien qu'une configuration supplémentaire soit nécessaire. Dans le contexte, cette solution ressemblerait à ceci:

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;
}
trémie
la source
Cela fonctionne comme un rêve avec Jackson 2.9.9. Il ne souffre pas d'une StackOverflowError comme l'autre exemple donné.
meta1203
2

Voici un oneliner utilisant 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;
}

Et s'il vous plaît: Il n'est vraiment pas nécessaire d'utiliser une valeur de chaîne ou autre chose. Toutes les informations nécessaires sont fournies par JsonParser, alors utilisez-le.

kaiser
la source
1

Je n'étais pas d'accord avec l'utilisation BeanSerializerModifiercar cela oblige à déclarer des changements de comportement dans le ObjectMapperdésérialiseur central plutôt que dans le désérialiseur personnalisé lui-même et en fait, c'est une solution parallèle pour annoter la classe d'entité avec JsonSerialize. Si vous le ressentez de la même manière, vous apprécierez peut-être ma réponse ici: https://stackoverflow.com/a/43213463/653539

Tomáš Záluský
la source
1

Une solution plus simple pour moi était simplement d'ajouter un autre bean de ObjectMapperet de l'utiliser pour désérialiser l'objet (grâce à https://stackoverflow.com/users/1032167/varren comment) - dans mon cas, j'étais intéressé soit à désérialiser son identifiant (un int) ou l'objet entier 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;
     }
}

pour chaque entité qui doit passer par un désérialiseur personnalisé, nous devons le configurer dans le ObjectMapperbean global de l'application Spring Boot dans mon cas (par exemple pour 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
la source
0

Vous êtes voué à l'échec si vous essayez de créer votre désérialiseur personnalisé à partir de zéro.

Au lieu de cela, vous devez obtenir l'instance de désérialiseur par défaut (entièrement configurée) via une instance personnalisée BeanDeserializerModifier, puis transmettre cette instance à votre classe de désérialiseur personnalisée:

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

Remarque: Cet enregistrement de module remplace l' @JsonDeserializeannotation, c'est-à-dire la Userclasse ouUser champs ne doivent plus être annotés avec cette annotation.

Le désérialiseur personnalisé doit alors être basé sur un DelegatingDeserializerafin que toutes les méthodes soient déléguées, sauf si vous fournissez une implémentation explicite:

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
la source
0

L'utilisation BeanDeserializerModifierfonctionne bien, mais si vous avez besoin d'utiliser, JsonDeserializeil existe un moyen de le faire AnnotationIntrospector comme ceci:

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

Maintenant, le mappeur copié ignorera votre désérialiseur personnalisé (MyDeserializer.class) et utilisera l'implémentation par défaut. Vous pouvez l'utiliser dans la deserializeméthode de votre désérialiseur personnalisé pour éviter la récursivité en rendant le mappeur copié statique ou en le câblant si vous utilisez Spring.

Filip Ljubičić
la source