web-dev-qa-db-fra.com

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

J'ai un problème dans mon désérialiseur personnalisé à Jackson. Je souhaite accéder au sérialiseur par défaut pour renseigner l'objet dans lequel je me 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 jackson par défaut.

C'est le code que j'ai pour le 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 de pouvoir pré-renseigner mon POJO avant de lancer ma logique spéciale.

Lorsque vous appelez désérialiseur depuis le désérialiseur personnalisé, la méthode est 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 sérialiseur de haricots, mais le processus est extrêmement complexe et je n'ai pas réussi à trouver le bon moyen de le faire. J'ai également essayé de surcharger inutilement l'introspecteur d'annotation, pensant que cela pourrait m'aider à ignorer l'annotation dans DeserializerContext. Enfin, il est possible que JsonDeserializerBuilders ait connu un certain succès, bien que cela me demande de faire des choses magiques pour comprendre le contexte de l’application à partir du printemps. J'apprécierais tout ce qui pourrait me mener à une solution plus propre, par exemple comment construire un contexte de désérialisation sans lire l'annotation JsonDeserializer.

79
Pablo Jomer

Comme StaxMan l'a déjà suggéré, vous pouvez le faire en écrivant une 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);
  }
}
75
schummar

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

Donc, ce que vous pouvez probablement utiliser est de construire une BeanDeserializerModifier, enregistrez-la via l’interface Module (utilisez SimpleModule). Vous devez définir/remplacer modifyDeserializer, et dans le cas spécifique où vous souhaitez ajouter votre propre logique (où le type correspond), construisez votre propre désérialiseur, transmettez le désérialiseur par défaut qui vous est donné . Et ensuite dans deserialize() méthode vous pouvez simplement déléguer appel, prenez le résultat Object.

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

Une autre façon qui pourrait fonctionner (mais pas à 100% sûr) serait de spécifier l'objet Converter (@JsonDeserialize(converter=MyConverter.class)). Ceci est une nouvelle fonctionnalité de Jackson 2.2 . Dans votre cas, Converter ne convertit pas le type, mais simplifie 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 être appelé en premier, et alors seulement votre Converter.

8
StaxMan

La DeserializationContext a une méthode readValue() 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.

Veillez simplement à appeler traverse() au niveau JsonNode que vous voulez lire pour récupérer la JsonParser à transmettre à 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;
    }

}
6
Derek Cochran

S'il vous est possible de déclarer une classe d'utilisateurs supplémentaire, vous pouvez l'implémenter en utilisant simplement 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;
  }

}
4
Bill

J'ai trouvé une réponse à https://stackoverflow.com/a/51927577/14731 qui est beaucoup plus lisible que la réponse acceptée.

public User deserialize(JsonParser jp, DeserializationContext ctxt)
    throws IOException, JsonProcessingException {
        JsonNode tree = jp.readTree(jp);

        // To call the default deserializer, simply invoke:
        User user = tree.get("user").traverse(jp.getCodec()).readValueAs(User.class);
        return user;
      }

Cela ne devient vraiment pas plus facile que cela.

1
Gili

Une solution plus simple pour moi consistait simplement à ajouter un autre bean de ObjectMapper et à l'utiliser pour désérialiser l'objet (grâce à https://stackoverflow.com/users/1032167/varren comment). soit désérialiser sur son id (un int), soit sur 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é devant passer par le désérialiseur personnalisé, nous devons la configurer dans le bean ObjectMapper 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;
}
1

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 vous procurer l'instance de désérialiseur par défaut (entièrement configurée) via une variable BeanDeserializerModifier personnalisée, puis la transmettre à votre classe de désérialiseur personnalisé:

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: cette inscription de module remplace l’annotation @JsonDeserialize; c’est-à-dire que les champs de la classe User ou User ne doivent plus être annotés avec cette annotation.

Le désérialiseur personnalisé doit alors être basé sur une DelegatingDeserializer afin que toutes les méthodes soient déléguées, à moins que vous ne fournissiez 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;
    }
}
0
oberlies

Je n'étais pas d'accord avec l'utilisation de BeanSerializerModifier dans la mesure où il oblige à déclarer certaines modifications de comportement dans le paramètre central ObjectMapper plutôt que dans le désérialiseur personnalisé. Il s'agit en fait d'une solution parallèle à l'annotation de la classe d'entités avec JsonSerialize. Si vous le sentez de la même manière, vous pourriez apprécier ma réponse ici: https://stackoverflow.com/a/43213463/653539

0

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 données par JsonParser, utilisez-les.

0
kaiser

Dans le sens de ce que Tomáš Záluský a suggéré , dans les cas où l'utilisation de BeanDeserializerModifier n'est pas souhaitable, vous pouvez créer vous-même un désérialiseur par défaut à l'aide de 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();
    JsonDeserializer<Object> defaultDeserializer = BeanDeserializerFactory.instance.buildBeanDeserializer(ctxt, User.class, config.introspect(User.class));

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