Как вызвать десериализатор по умолчанию из настраиваемого десериализатора в Джексоне

105

У меня проблема с моим пользовательским десериализатором в Джексоне. Я хочу получить доступ к сериализатору по умолчанию, чтобы заполнить объект, в который я десериализуюсь. После пополнения я сделаю некоторые пользовательские вещи, но сначала я хочу десериализовать объект с поведением Джексона по умолчанию.

Это код, который у меня есть на данный момент.

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

}

Мне нужен способ инициализировать десериализатор по умолчанию, чтобы я мог предварительно заполнить свой POJO, прежде чем я начну свою особую логику.

При вызове десериализации из пользовательского десериализатора Кажется, что метод вызывается из текущего контекста независимо от того, как я создаю класс сериализатора. Из-за аннотации в моем POJO. Это вызывает исключение Stack Overflow по очевидным причинам.

Я попытался инициализировать a, BeanDeserializerно процесс чрезвычайно сложен, и мне не удалось найти правильный способ сделать это. Я также безуспешно пытался перегрузить AnnotationIntrospectorфайл, думая, что это может помочь мне игнорировать аннотацию в DeserializerContext. Наконец, похоже, что я мог бы добиться успеха, JsonDeserializerBuildersхотя для этого мне потребовалось кое-что волшебное, чтобы получить контекст приложения из Spring. Я был бы признателен за любую вещь, которая могла бы привести меня к более чистому решению, например, как я могу создать контекст десериализации, не читая JsonDeserializerаннотацию.

Пабло Йомер
источник
2
Нет. Эти подходы не помогут: проблема в том, что вам понадобится полностью сконструированный десериализатор по умолчанию; а для этого требуется, чтобы он был построен, а затем ваш десериализатор получил к нему доступ. DeserializationContextэто не то, что вы должны создавать или изменять; это будет предоставлено ObjectMapper. AnnotationIntrospector, также не поможет в получении доступа.
StaxMan
Как вы в итоге это сделали?
khituras
Хороший вопрос. Я не уверен, но уверен, что приведенный ниже ответ мне помог. В настоящее время я не владею кодом, который мы написали, если вы все же найдете решение, пожалуйста, опубликуйте его здесь для других.
Пабло Джомер

Ответы:

93

Как уже предлагал StaxMan, вы можете сделать это, написав BeanDeserializerModifierи зарегистрировав его через SimpleModule. Следующий пример должен работать:

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);
  }
}
шуммар
источник
Спасибо! Я уже решил это другим способом, но я изучу ваше решение, когда у меня будет больше времени.
Пабло Джомер
5
Есть ли способ сделать то же самое, но с a JsonSerializer? У меня есть несколько сериализаторов, но у них есть общий код, поэтому я хочу его обобщить. Я пытаюсь напрямую вызвать сериализатор, но результат не разворачивается в результате JSON (каждый вызов сериализатора создает новый объект)
herau
1
@herau BeanSerializerModifier, ResolvableSerializerи ContextualSerializerявляются согласующие интерфейсы , чтобы использовать для сериализации.
StaxMan
Применимо ли это к контейнерам EE edition (Wildfly 10)? Я получаю JsonMappingException: (было java.lang.NullPointerException) (через цепочку ссылок: java.util.ArrayList [0])
user1927033
В вопросе используется, readTree()а в ответе нет. В чем преимущество этого подхода по сравнению с тем, что опубликовал Дерек Кокран ? Есть ли способ заставить это работать readTree()?
Gili
15

Я нашел ответ на ans, который намного читабельнее, чем принятый ответ.

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

На самом деле нет ничего проще.

Гили
источник
Привет, Гили! Спасибо за это. Я надеюсь, что люди найдут этот ответ и успеют его проверить. Я больше не в состоянии сделать это там, потому что не могу принять ответ сейчас. Если я увижу, что люди действительно говорят, что это возможное решение, я, конечно, направлю их к нему. Также может быть, что это возможно не для всех версий. Тем не менее, спасибо, что поделились.
Пабло Джомер
Не компилируется с Jackson 2.9.9. JsonParser.readTree () не существует.
ccleve
@ccleve Похоже на простую опечатку. Исправлена.
Гили
Могу подтвердить, что это работает с Jackson 2.10, спасибо!
Стюарт Лейланд-Коул,
3
Я не понимаю, как это работает, это приводит к a StackOverflowError, поскольку Джексон снова будет использовать тот же сериализатор для User...
john16384
12

У DeserializationContextнего есть readValue()метод, который вы можете использовать. Это должно работать как для десериализатора по умолчанию, так и для любых пользовательских десериализаторов, которые у вас есть.

Просто не забудьте вызвать traverse()на JsonNodeуровне вы хотите прочитать , чтобы получить , JsonParserчтобы перейти к 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 () не существует, это метод ObjectMapper
Педро Борхес
это решение работает хорошо, однако вам может потребоваться вызвать nextToken (), если вы десериализуете класс значений, например Date.class
revau.lt
9

Есть несколько способов сделать это, но чтобы сделать это правильно, потребуется немного больше работы. По сути, вы не можете использовать подклассы, поскольку информация, необходимая для десериализаторов по умолчанию, строится из определений классов.

Итак, что вы, скорее всего, можете использовать, так это создать BeanDeserializerModifier, зарегистрировать его через Moduleинтерфейс (использовать SimpleModule). Вам нужно определить / переопределить modifyDeserializer, а в конкретном случае, когда вы хотите добавить свою собственную логику (где тип совпадает), создать свой собственный десериализатор, передать десериализатор по умолчанию, который вам предоставлен. А затем в deserialize()методе вы можете просто делегировать вызов, взять объект результата.

В качестве альтернативы, если вы действительно должны создать и заполнить объект, вы можете сделать это и вызвать перегруженную версию, deserialize()которая принимает третий аргумент; объект для десериализации.

Другой способ, который может сработать (но не на 100% уверен), - это указать Converterobject ( @JsonDeserialize(converter=MyConverter.class)). Это новая функция Jackson 2.2. В вашем случае Converter на самом деле не будет преобразовывать тип, а упрощает изменение объекта: но я не знаю, позволит ли это вам делать именно то, что вы хотите, поскольку сначала будет вызываться десериализатор по умолчанию, а только затем ваш Converter.

StaxMan
источник
Мой ответ по-прежнему остается в силе: вам нужно позволить Джексону создать десериализатор по умолчанию, которому он будет делегировать; и нужно найти способ "отменить" это. BeanDeserializerModifier- это обработчик обратного вызова, который позволяет это.
StaxMan
7

Если вы можете объявить дополнительный класс User, вы можете реализовать его, просто используя аннотации

// 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
Ага. Единственный подход, который сработал для меня. Я получал StackOverflowErrors из-за рекурсивного вызова десериализатора.
ccleve
3

В соответствии с тем, что предложил Томаш Залуски , в случаях, когда использование BeanDeserializerModifierнежелательно, вы можете самостоятельно сконструировать десериализатор по умолчанию BeanDeserializerFactory, хотя для этого потребуется дополнительная настройка. В контексте это решение могло бы выглядеть так:

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;
}
бункер
источник
Это работает как мечта с Jackson 2.9.9. Он не страдает от StackOverflowError, как в другом приведенном примере.
meta1203
2

Вот один лайнер с использованием 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;
}

И пожалуйста: действительно нет необходимости использовать какое-либо значение String или что-то еще. Вся необходимая информация предоставляется JsonParser, поэтому используйте ее.

кайзер
источник
1

Я был не в порядке с использованием, BeanSerializerModifierпоскольку он заставляет объявлять некоторые поведенческие изменения в центральном, ObjectMapperа не в самом настраиваемом десериализаторе, и на самом деле это параллельное решение для аннотирования класса сущности с помощью JsonSerialize. Если вы чувствуете это так же, вы можете оценить мой ответ здесь: https://stackoverflow.com/a/43213463/653539

Томаш Залуски
источник
1

Более простым решением для меня было просто добавить еще один bean-компонент ObjectMapperи использовать его для десериализации объекта (благодаря комментарию https://stackoverflow.com/users/1032167/varren ) - в моем случае мне было интересно либо десериализовать его по идентификатору (int) или весь объект 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;
     }
}

для каждого объекта, который должен проходить через настраиваемый десериализатор, нам нужно настроить его в глобальном ObjectMapperbean-компоненте приложения Spring Boot в моем случае (например, для 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

Вы обречены на провал, если попытаетесь создать собственный десериализатор с нуля.

Вместо этого вам нужно получить (полностью настроенный) экземпляр десериализатора по умолчанию с помощью настраиваемого BeanDeserializerModifier, а затем передать этот экземпляр в свой собственный класс десериализатора:

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

Примечание. Эта регистрация модуля заменяет @JsonDeserializeаннотацию, т. Е. UserКласс или Userполя больше не должны быть аннотированы этой аннотацией.

Пользовательский десериализатор должен быть основан на, DelegatingDeserializerчтобы все методы делегировали, если вы не предоставили явную реализацию:

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

Использование BeanDeserializerModifierработает хорошо, но если вам нужно использовать, JsonDeserializeесть способ сделать это AnnotationIntrospector следующим образом:

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

Теперь скопированный маппер будет игнорировать ваш пользовательский десериализатор (MyDeserializer.class) и будет использовать реализацию по умолчанию. Вы можете использовать его внутри deserializeметода вашего настраиваемого десериализатора, чтобы избежать рекурсии, сделав скопированный сопоставитель статическим или подключив его при использовании Spring.

Филип Любичич
источник