Como faço para chamar o desserializador padrão de um desserializador personalizado em Jackson

105

Tenho um problema no meu desserializador personalizado em Jackson. Desejo acessar o serializador padrão para preencher o objeto para o qual estou desserializando. Depois da população, farei algumas coisas personalizadas, mas primeiro quero desserializar o objeto com o comportamento padrão de Jackson.

Este é o código que tenho no 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;
  }

}

O que eu preciso é de uma maneira de inicializar o desserializador padrão para que eu possa pré-preencher meu POJO antes de iniciar minha lógica especial.

Ao chamar desserializar de dentro do desserializador personalizado Parece que o método é chamado a partir do contexto atual, não importa como eu construo a classe do serializador. Por causa da anotação no meu POJO. Isso causa uma exceção Stack Overflow por razões óbvias.

Tentei inicializar um, BeanDeserializermas o processo é extremamente complexo e não consegui encontrar a maneira certa de fazer isso. Também tentei sobrecarregar o AnnotationIntrospectorsem sucesso, pensando que isso poderia me ajudar a ignorar a anotação no DeserializerContext. Por fim, parece que tive algum sucesso com o uso, JsonDeserializerBuildersembora isso exigisse que eu fizesse algumas coisas mágicas para obter o contexto do aplicativo do Spring. Eu apreciaria qualquer coisa que pudesse me levar a uma solução mais limpa, por exemplo, como posso construir um contexto de desserialização sem ler a JsonDeserializeranotação.

Pablo Jomer
fonte
2
Não. Essas abordagens não ajudarão: o problema é que você precisará de um desserializador padrão totalmente construído; e isso requer que um seja criado e, em seguida, seu desserializador tenha acesso a ele. DeserializationContextnão é algo que você deve criar ou alterar; será fornecido por ObjectMapper. AnnotationIntrospector, da mesma forma, não ajudará na obtenção de acesso.
StaxMan de
Como você acabou fazendo isso no final?
khituras
Boa pergunta. Não tenho certeza, mas tenho certeza de que a resposta abaixo me ajudou. No momento, não estou de posse do código que escrevemos. Se você encontrar uma solução, poste-o aqui para outras pessoas.
Pablo Jomer

Respostas:

93

Como StaxMan já sugeriu, você pode fazer isso escrevendo um BeanDeserializerModifier e registrando-o via SimpleModule. O seguinte exemplo deve funcionar:

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
fonte
Obrigado! Já resolvi isso de outra forma, mas irei buscar sua solução quando tiver mais tempo.
Pablo Jomer
5
Existe uma maneira de fazer o mesmo, mas com um JsonSerializer? Eu tenho vários serializadores, mas eles têm código em comum, então quero generalizá-lo. Tento chamar diretamente o serializador, mas o resultado não é desembrulhado no resultado JSON (cada chamada do serializador cria um novo objeto)
herau
1
@herau BeanSerializerModifier, ResolvableSerializere ContextualSerializersão as interfaces de correspondência a ser usado para serialização.
StaxMan
Isso é aplicável para contêineres da edição EE (Wildfly 10)? Recebo JsonMappingException: (era java.lang.NullPointerException) (por meio da cadeia de referência: java.util.ArrayList [0])
user1927033 de
A pergunta usa, readTree()mas a resposta não. Qual é a vantagem dessa abordagem em relação à postada por Derek Cochran ? Existe uma maneira de fazer isso funcionar readTree()?
Gili
14

Encontrei uma resposta em ans que é muito mais legível do que a resposta aceita.

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

Realmente não pode ser mais fácil do que isso.

Gili
fonte
Oi Gili! Obrigado por isso, espero que as pessoas encontrem essa resposta e tenham tempo para validá-la. Não estou mais em posição de fazê-lo lá, pois não posso aceitar a resposta no momento. Se eu vir que as pessoas dizem que essa é uma solução possível, é claro que as guiarei nessa direção. Também pode ser que isso não seja possível para todas as versões. Ainda obrigado por compartilhar.
Pablo Jomer
Não compila com Jackson 2.9.9. JsonParser.readTree () não existe.
ccleve
@ccleve Parece um erro de digitação simples. Fixo.
Gili
Posso confirmar que isso funciona com Jackson 2.10, obrigado!
Stuart Leyland-Cole
2
Não entendi como isso funciona, o que resulta em um StackOverflowError, já que Jackson usará o mesmo serializador novamente para User...
john16384
12

O DeserializationContexttem um readValue()método que você pode usar. Isso deve funcionar para o desserializador padrão e qualquer desserializador personalizado que você tiver.

Apenas certifique-se de chamar traverse()no JsonNodenível que deseja ler para recuperar o JsonParserpara passar 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
fonte
DeserialisationContext.readValue () não existe, que é um método de ObjectMapper
Pedro Borges
esta solução está funcionando bem, no entanto, você pode precisar chamar nextToken () se você desserializar uma classe de valor, por exemplo, Date.class
revau.lt
9

Existem algumas maneiras de fazer isso, mas fazer certo envolve um pouco mais de trabalho. Basicamente, você não pode usar a subclasse, uma vez que as informações de que os desserializadores padrão precisam é criado a partir de definições de classe.

Portanto, o que você provavelmente pode usar é construir um BeanDeserializerModifier, registrá-lo via Moduleinterface (use SimpleModule). Você precisa definir / substituir modifyDeserializere, para o caso específico em que deseja adicionar sua própria lógica (em que o tipo corresponde), construir seu próprio desserializador, passar o desserializador padrão que você recebeu. E então, no deserialize()método, você pode simplesmente delegar a chamada, pegar o Object resultante.

Alternativamente, se você realmente deve criar e preencher o objeto, você pode fazer isso e chamar a versão sobrecarregada deserialize()que leva o terceiro argumento; objeto para desserializar.

Outra maneira que pode funcionar (mas não 100% certa) seria especificar Converterobject ( @JsonDeserialize(converter=MyConverter.class)). Este é um novo recurso do Jackson 2.2. No seu caso, o Converter não converteria realmente o tipo, mas simplificaria a modificação do objeto: mas não sei se isso permitiria que você fizesse exatamente o que deseja, já que o desserializador padrão seria chamado primeiro, e só depois o seu Converter.

StaxMan
fonte
Minha resposta ainda permanece: você precisa deixar Jackson construir o desserializador padrão para o qual delegar; e tem que encontrar uma maneira de "substituí-lo". BeanDeserializerModifieré o manipulador de retorno de chamada que permite isso.
StaxMan de
7

Se for possível para você declarar uma classe de usuário extra, você pode implementá-la apenas usando anotações

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

}
Conta
fonte
1
Sim. A única abordagem que funcionou para mim. Eu estava recebendo StackOverflowErrors por causa de uma chamada recursiva para o desserializador.
ccleve
3

Seguindo as linhas do que Tomáš Záluský sugeriu , nos casos em que o uso BeanDeserializerModifieré indesejável, você pode construir um desserializador padrão usando BeanDeserializerFactory, embora haja alguma configuração extra necessária. No contexto, essa solução seria assim:

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;
}
tremonha
fonte
Isso funciona como um sonho com Jackson 2.9.9. Ele não sofre de StackOverflowError como o outro exemplo dado.
meta1203
2

Aqui está um oneliner usando 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 por favor: realmente não há necessidade de usar nenhum valor String ou qualquer outra coisa. Todas as informações necessárias são fornecidas pelo JsonParser, então use-o.

Kaiser
fonte
1

Eu não estava ok com o uso, BeanSerializerModifierpois força a declarar algumas mudanças comportamentais no central em ObjectMappervez do próprio desserializador personalizado e, de fato, é uma solução paralela para anotar classes de entidade com JsonSerialize. Se você acha que é da mesma forma, pode gostar de minha resposta aqui: https://stackoverflow.com/a/43213463/653539

Tomáš Záluský
fonte
1

Uma solução mais simples para mim era apenas adicionar outro feijão de ObjectMapper e usá-lo para desserializar o objeto (graças ao https://stackoverflow.com/users/1032167/varren comentário) - no meu caso, estava interessado em desserializar para seu id (um int) ou o objeto inteiro 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;
     }
}

para cada entidade que precisa passar pelo desserializador personalizado, precisamos configurá-la no ObjectMapperbean global do Spring Boot App no ​​meu caso (por exemplo, para 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
fonte
0

Você está fadado ao fracasso se tentar criar seu desserializador personalizado do zero.

Em vez disso, você precisa obter a instância do desserializador padrão (totalmente configurada) por meio de um personalizado BeanDeserializerModifiere, em seguida, passar essa instância para sua classe do desserializador personalizado:

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: O registro deste módulo substitui a @JsonDeserializeanotação, ou seja, a Userclasse ou os Usercampos não devem mais ser anotados com esta anotação.

O desserializador personalizado deve ser baseado em um DelegatingDeserializerpara que todos os métodos sejam delegados, a menos que você forneça uma implementação explícita:

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
fonte