¿Cómo llamo al deserializador predeterminado desde un deserializador personalizado en Jackson?

105

Tengo un problema en mi deserializador personalizado en Jackson. Quiero acceder al serializador predeterminado para completar el objeto en el que estoy deserializando. Después de la población, haré algunas cosas personalizadas, pero primero quiero deserializar el objeto con el comportamiento predeterminado de Jackson.

Este es el código que tengo en este 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;
  }

}

Lo que necesito es una forma de inicializar el deserializador predeterminado para poder rellenar previamente mi POJO antes de iniciar mi lógica especial.

Al llamar a deserializar desde dentro del deserializador personalizado, parece que el método se llama desde el contexto actual sin importar cómo construya la clase de serializador. Por la anotación en mi POJO. Esto provoca una excepción de desbordamiento de pila por razones obvias.

He intentado inicializar un, BeanDeserializerpero el proceso es extremadamente complejo y no he logrado encontrar la manera correcta de hacerlo. También intenté sobrecargar AnnotationIntrospectorsin éxito, pensando que podría ayudarme a ignorar la anotación en DeserializerContext. Finalmente, parece que podría haber tenido cierto éxito con el uso, JsonDeserializerBuildersaunque esto me obligó a hacer algunas cosas mágicas para obtener el contexto de la aplicación de Spring. Agradecería cualquier cosa que pudiera llevarme a una solución más limpia, por ejemplo, ¿cómo puedo construir un contexto de deserialización sin leer la JsonDeserializeranotación?

Pablo Jomer
fuente
2
No. Esos enfoques no ayudarán: el problema es que necesitará un deserializador predeterminado completamente construido; y esto requiere que se construya uno y luego su deserializador obtenga acceso a él. DeserializationContextno es algo que debas crear o cambiar; será proporcionado por ObjectMapper. AnnotationIntrospector, del mismo modo, no será de ayuda para obtener acceso.
StaxMan
¿Cómo terminaste haciéndolo al final?
khituras
Buena pregunta. No estoy seguro, pero estoy seguro de que la respuesta a continuación me ayudó. Actualmente no tengo el código que escribimos. Si encuentra una solución, publíquelo aquí para otros.
Pablo Jomer

Respuestas:

92

Como StaxMan ya sugirió, puede hacer esto escribiendo un BeanDeserializerModifiery registrándolo a través de SimpleModule. El siguiente ejemplo debería 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
fuente
¡Gracias! Ya resolví esto de otra manera, pero buscaré tu solución cuando tenga más tiempo.
Pablo Jomer
5
¿Hay alguna forma de hacer lo mismo pero con un JsonSerializer? Tengo varios serializadores pero tienen código en común, así que quiero generarlo. Intento llamar directamente al serializador, pero el resultado no se desenvuelve en el resultado JSON (cada llamada al serializador crea un nuevo objeto)
herau
1
@herau BeanSerializerModifier, ResolvableSerializery ContextualSerializerson las interfaces coincidentes que se utilizarán para la serialización.
StaxMan
¿Es esto aplicable a los contenedores de la edición EE (Wildfly 10)? Recibo JsonMappingException: (era java.lang.NullPointerException) (a través de la cadena de referencia: java.util.ArrayList [0])
user1927033
La pregunta usa readTree()pero la respuesta no. ¿Cuál es la ventaja de este enfoque frente al publicado por Derek Cochran ? ¿Hay alguna manera de hacer que esto funcione readTree()?
Gili
14

Encontré una respuesta en ans que es mucho más legible que la respuesta aceptada.

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

Realmente no hay nada más fácil que esto.

Gili
fuente
¡Hola Gili! Gracias por ello. Espero que la gente encuentre esta respuesta y tenga tiempo para validarla. Ya no estoy en condiciones de hacerlo allí porque no puedo aceptar la respuesta en este momento. Si veo que la gente dice que esta es una posible solución, por supuesto, los guiaré hacia ella. También puede ser que esto no sea posible para todas las versiones. Aún así, gracias por compartir.
Pablo Jomer
No se compila con Jackson 2.9.9. JsonParser.readTree () no existe.
ccleve
@ccleve Parece un simple error tipográfico. Fijo.
Gili
Puedo confirmar que esto funciona con Jackson 2.10, ¡gracias!
Stuart Leyland-Cole
1
No entiendo cómo funciona esto, esto da como resultado un StackOverflowError, ya que Jackson usará el mismo serializador nuevamente para User...
john16384
12

El DeserializationContexttiene un readValue()método que puede usar. Esto debería funcionar tanto para el deserializador predeterminado como para cualquier deserializador personalizado que tenga.

Solo asegúrese de llamar traverse()al JsonNodenivel que desea leer para recuperar el JsonParserpara pasar 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
fuente
DeserialisationContext.readValue () no existe, ese es un método de ObjectMapper
Pedro Borges
esta solución funciona bien, sin embargo, es posible que deba llamar a nextToken () si deserializa una clase de valor, por ejemplo, Date.class
revau.lt
9

Hay un par de formas de hacer esto, pero hacerlo bien implica un poco más de trabajo. Básicamente, no puede utilizar la subclasificación, ya que la información que necesitan los deserializadores predeterminados se crea a partir de definiciones de clases.

Entonces, lo que probablemente pueda usar es construir un BeanDeserializerModifierregistro a través de la Moduleinterfaz (uso SimpleModule). Debe definir / anular modifyDeserializer, y para el caso específico en el que desea agregar su propia lógica (donde el tipo coincide), construya su propio deserializador, pase el deserializador predeterminado que se le proporciona. Y luego, en el deserialize()método, puede simplemente delegar la llamada, tomar el resultado Object.

Alternativamente, si realmente debe crear y completar el objeto, puede hacerlo y llamar a la versión sobrecargada deserialize()que toma el tercer argumento; objeto para deserializar.

Otra forma que podría funcionar (pero no 100% segura) sería especificar Converterobject ( @JsonDeserialize(converter=MyConverter.class)). Esta es una nueva característica de Jackson 2.2. En su caso, Converter no convertiría el tipo en realidad, sino que simplificaría la modificación del objeto: pero no sé si eso le permitiría hacer exactamente lo que desea, ya que el deserializador predeterminado se llamaría primero, y solo luego su Converter.

StaxMan
fuente
Mi respuesta sigue en pie: debe permitir que Jackson construya el deserializador predeterminado al que delegar; y tener que encontrar una manera de "anularlo". BeanDeserializerModifieres el controlador de devolución de llamada que permite eso.
StaxMan
7

Si es posible que declare una clase de usuario adicional, puede implementarla simplemente usando anotaciones

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

}
Cuenta
fuente
1
Sip. El único enfoque que funcionó para mí. Obtenía StackOverflowErrors debido a una llamada recursiva al deserializador.
ccleve
2

Aquí hay un delineador 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;
}

Y por favor: Realmente no hay necesidad de usar ningún valor de cadena o algo más. Toda la información necesaria la proporciona JsonParser, así que úsela.

emperador
fuente
2

En la línea de lo que ha sugerido Tomáš Záluský , en los casos en que el uso no BeanDeserializerModifiersea ​​deseable, puede construir un deserializador predeterminado usted mismo usando BeanDeserializerFactory, aunque es necesaria una configuración adicional. En contexto, esta solución se vería así:

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;
}
tolva
fuente
1

No estaba de acuerdo con el uso, BeanSerializerModifierya que obliga a declarar algunos cambios de comportamiento ObjectMapperen el deserializador central en lugar del personalizado y, de hecho, es una solución paralela para anotar la clase de entidad con JsonSerialize. Si lo siente de manera similar, puede apreciar mi respuesta aquí: https://stackoverflow.com/a/43213463/653539

Tomáš Záluský
fuente
1

Una solución más simple para mí fue simplemente agregar otro bean de ObjectMappery usarlo para deserializar el objeto (gracias a https://stackoverflow.com/users/1032167/varren comentario); en mi caso, estaba interesado en deserializar a su id (un int) o el objeto completo 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 entidad que necesita pasar por un deserializador personalizado, debemos configurarlo en el ObjectMapperbean global de la aplicación Spring Boot en mi caso (por ejemplo, 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
fuente
0

Es probable que falle si intenta crear su deserializador personalizado desde cero.

En su lugar, debe obtener la instancia de deserializador predeterminada (completamente configurada) a través de una instancia personalizada BeanDeserializerModifiery luego pasar esta instancia a su clase de deserializador personalizada:

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: El registro de este módulo reemplaza la @JsonDeserializeanotación, es decir, la Userclase o los Usercampos ya no deben anotarse con esta anotación.

El deserializador personalizado debe basarse en un DelegatingDeserializerpara que todos los métodos deleguen, a menos que proporcione una implementación 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
fuente