Spring/Jackson + @JsonView: фильтруем JSON

  • Tutorial

Здравствуйте!

Недавно в своем учебном Spring проекте Java Enterprise (Topjava) столкнулся с задачей каcтомизации сериализации объекта User в JSON в зависимости от контроллера: для REST API контроллера нужно было возвращать хешированный пароль (поле user.password), а для контроллера отображения на UI- нет. Можно решить задачу в «лоб», сделав нестолько TO (Data Transfer Object), но в Spring 4.2+/Jackson 2.6 появилась возможность использовать Jackson’s Serialization Views. Однако с статье есть подвох, и для невнимательных читателей вьюхи работают не так, как он ожидает.

В результате мне пришлось немного покопаться в реализации Jackson, чтобы понять, как все это работает. Коротко об этом:

MapperFeature.DEFAULT_VIEW_INCLUSION



В статье есть небольшое упоминание
In Spring MVC default configuration, MapperFeature.DEFAULT_VIEW_INCLUSION is set to false.

Это означает, что по умолчанию поля, не помеченные аннотацией @JsonView, исключаются. Но если посмотреть в код MapperFeature, то увидим:

    ...
    * Default value is enabled, meaning that non-annotated
    * properties are included in all views if there is no
    * {@link com.fasterxml.jackson.annotation.JsonView} annotation.
    *
    * Feature is enabled by default.
    */
    DEFAULT_VIEW_INCLUSION(true),

Т.е все с точностью до наоборот — все, что не помечено, включается. И если пометить только нужные для UI поля User:

public class User
    ...
    @JsonView(View.UI.class)
    protected String email;

    @JsonView(View.UI.class)
    protected boolean enabled = true;

    protected String password;

и вызвать помеченный @JsonView метод контроллера

@JsonView(View.UI.class)
@RequestMapping(value = "/{id}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
public User get(@PathVariable("id") int id) {
   return ...;
}

То в результат войдут как помеченные поля User (email, enabled, ..), так и все остальные (password).

FilteredBeanPropertyWriter


Т.к. хочется исключить из контроллера UI только одно поле password, логично будет пометить только его. Смотрим в код jackson-databind-2.8.0: если запрос контроллера и поля его результата аннотированы @JsonView, Jackson сериализует через FilteredBeanPropertyWriter.serializeAsField

    final Class<?> activeView = prov.getActiveView();
    if (activeView != null) {
        int i = 0, len = _views.length;
        for (; i < len; ++i) {
           if (_views[i].isAssignableFrom(activeView)) break;
        }
        // not included, bail out:
        if (i == len) {
            _delegate.serializeAsOmittedField(bean, jgen, prov);
            return;
        }
    }
    _delegate.serializeAsField(bean, jgen, prov);

Т.е. если View, которым помечено поле объекта, совпадает или является суперклассом от View метода контроллера, поле сериализуется. Иначе оно пропускается (serializeAsOmittedField).

Решение


В итоге:

  • создаем по одному View для каждого контекста сериализации

    
    public class View {
        public static class REST {}
        public static class UI {}
    }

  • помечаем в User исключаемые в UI поля тем View, в котором они должны присутствовать (REST)

    
    public class User
        ...
    
        protected String email;
    
        protected boolean enabled = true;
    
        @JsonView(View.REST.class)
        protected String password;

  • аннотируем метод контроллера UI соответствующим контекстом

    @JsonView(View.UI.class)
    @RequestMapping(value = "/{id}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
    public User get(@PathVariable("id") int id) {
       return ...;
    }

Теперь поле password в результат не попадет. В контроллере REST можно обойтись без @JsonView, т.к. туда включаются все поля User.

Спасибо за внимание! Надеюсь @JsonView сделают Ваши Spring приложения более красивыми и компактными.
  • +15
  • 14,4k
  • 5
Поделиться публикацией
Комментарии 5
    0
    Интересно стало, для каких целей отправляете хеш пароля в REST пользователю? Или это просто пример?
      0
      Скорее да, как пример. Но вообще безнес требования бывают совершенно разные, и это больше задача ПМ.
      0
      Старый добрый TO подход все же луше как по мне, он универсален, из TO потом в любой формат просто транслировать, а здесь частный случай — JSON.
        0
        1. Счас достаточно много примеров проектов, в которых вообще отсутствует TO
        2. Мы же можем TO также аннотировать @JsonView. Если контекстов много, то придется на каждый из них делать свои методы в сервисе, или возможно — даже свой сервис. Или пергонять в TO в контроллере, что возможно блюстетелям строгих правил тоже может не понравится.
          0
          Я уже использовал эти JsonView в нескольких проектах (никаких TO, аннотируются прямо JPA модели), пока что мне такой подход не слишком нравится, но для простых проектов жизнеспособно.

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое