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

76d298a537be46578f80b666a4bf6388.pngЗдравствуйте!

Недавно в проекте Spring столкнулся с задачей катомизации сериализации объекта 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 приложения более красивыми и компактными.

Комментарии (1)

  • 9 августа 2016 в 10:21

    0

    Интересно стало, для каких целей отправляете хеш пароля в REST пользователю? Или это просто пример?

© Habrahabr.ru