Настройка валидации DTO в Spring Framework

    Всем привет! Сегодня мы коснёмся валидации данных, входящих через Data Transfer Object (DTO), настроим аннотации и видимости — так, чтобы получать и отдавать только то, что нам нужно.

    Итак, у нас есть DTO-класс UserDto, с соответствующими полями:

    public class UserDto {
    
        private Long id;
        private String name;
        private String login;
        private String password;
        private String email;
    }

    Я опускаю конструкторы и геттеры-сеттеры — уверен, вы умеете их создавать, а увеличивать в 3-4 раза код смысла не вижу — представим, что они уже есть.

    Мы будем принимать DTO через контроллер с CRUD-методами. Опять же, я не буду писать все методы CRUD — для чистоты эксперимента нам хватит пары. Пусть это будут create и updateName.

        @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces = 
        MediaType.APPLICATION_JSON_VALUE)
        public ResponseEntity<UserDto> create(@RequestBody UserDto dto) {
           return new ResponseEntity<>(service.save(dto), HttpStatus.OK);
        }
    
        @PutMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces = 
        MediaType.APPLICATION_JSON_VALUE)
        public ResponseEntity<UserDto> updateName(@RequestBody UserDto dto) {
           return new ResponseEntity<>(service.update(dto), HttpStatus.OK);
        }

    Для наглядности их тоже пришлось упростить. Таким образом, мы получаем какой-то JSON, который преобразуется в UserDto, и возвращаем UserDto, который преобразуется в JSON и отправляется на клиент.

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

        @Null //значение должно быть null
        @NotNull //значение должно быть не null
        @Email //это должен быть e-mail

    Со всеми аннотациями можно ознакомиться в библиотеке javax.validation.constraints. Итак, настроим наше DTO таким образом, чтобы сразу получать валидированый объект для дальнейшего перевода в сущность и сохранения в БД. Те поля, которые должны быть заполнены, мы пометим NotNull, также пометим e-mail:

    public class UserDto {
    
        @Null //автогенерация в БД
        private Long id;
    
        @NotNull
        private String name;
    
        @NotNull
        private String login;
    
        @NotNull
        private String password;
    
        @NotNull
        @Email
        private String email;
    }

    Мы задали настройки валидации для DTO — должны быть заполнены все поля, кроме id — он генерируется в БД. Добавим валидацию в контроллер:

        @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces = 
        MediaType.APPLICATION_JSON_VALUE)
        public ResponseEntity<UserDto> create(@Validated @RequestBody UserDto dto) {
           return new ResponseEntity<>(service.save(dto), HttpStatus.OK);
        }
    
        @PutMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces = 
        MediaType.APPLICATION_JSON_VALUE)
        public ResponseEntity<UserDto> updateName(@Validated @RequestBody UserDto dto) {
           return new ResponseEntity<>(service.update(dto), HttpStatus.OK);
        }

    Настроенная таким образом валидация подойдёт к созданию нового пользователя, но не подойдёт для обновления существующих — ведь для этого нам нужно будет получить id (который задан как null), а также, пропустить поля login, password и email, поскольку в updateName мы изменяем только имя. То есть, нам нужно получить id и name, и ничего больше. И здесь нам потребуются интерфейсы видимости.

    Создадим прямо в классе DTO интерфейс (для наглядности, я рекомендую выносить такие вещи в отдельный класс, а лучше, в отдельный пакет, например, transfer). Интерфейс будет называться New, второй будет называться Exist, от которого мы унаследуем UpdateName (в дальнейшем мы сможем наследовать от Exist другие интерфейсы видимости, мы же не одно имя будем менять):

    public class User {
    
        interface New {
        }
    
        interface Exist {
        }
        
        interface UpdateName extends Exist {
        }
    
        @Null //автогенерация в БД
        private Long id;
    
        @NotNull
        private String name;
    
        @NotNull
        private String login;
    
        @NotNull
        private String password;
    
        @NotNull
        @Email
        private String email;
    }

    Теперь мы пометим наши аннотации интерфейсом New.

        @Null(groups = {New.class})
        private Long id;
    
        @NotNull(groups = {New.class})
        private String name;
    
        @NotNull(groups = {New.class})
        private String login;
    
        @NotNull(groups = {New.class})
        private String password;
    
        @NotNull(groups = {New.class})
        @Email(groups = {New.class})
        private String email;

    Теперь эти аннотации работают только при указании интерфейса New. Нам остаётся только задать аннотации для того случая, когда нам потребуется апдейтить поле name (напомню, нам нужно указать не-нулловвыми id и name, остальные нулловыми). Вот как это выглядит:

        @Null(groups = {New.class})
        @NotNull(groups = {UpdateName.class})
        private Long id;
    
        @NotNull(groups = {New.class, UpdateName.class})
        private String name;
    
        @NotNull(groups = {New.class})
        @Null(groups = {UpdateName.class})
        private String login;
    
        @NotNull(groups = {New.class})
        @Null(groups = {UpdateName.class})
        private String password;
    
        @NotNull(groups = {New.class})
        @Null(groups = {UpdateName.class})
        @Email(groups = {New.class})
        private String email;

    Теперь нам осталось задать необходимые настройки в контроллерах, прописать интерфейс, чтобы задать валидацию:

        @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces = 
        MediaType.APPLICATION_JSON_VALUE)
        public ResponseEntity<UserDto> create(@Validated(UserDto.New.class) @RequestBody UserDto dto) {
           return new ResponseEntity<>(service.save(dto), HttpStatus.OK);
        }
    
        @PutMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces = 
        MediaType.APPLICATION_JSON_VALUE)
        public ResponseEntity<UserDto> updateName(@Validated(UserDto.UpdateName.class) @RequestBody 
    UserDto dto) {
           return new ResponseEntity<>(service.update(dto), HttpStatus.OK);
        }

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

    Итак, мы разобрались, как валидировать входные данные, теперь осталось валидировать выходные. Это делается при помощи аннотации @JsonView.

    Сейчас в выходном DTO, который мы отдаём обратно, содержатся все поля. Но, предположим, нам не нужно никогда отдавать пароль (кроме исключительных случаев).

    Для валидации выходного DTO добавим ещё два интерфейса, которые будут отвечать за видимость выходных данных — Details (для отображения пользователям) и AdminDetails (для отображения только админам). Интерфейсы могут наследоваться друг от друга, но для простоты восприятия сейчас мы делать этого не будем — достаточно примера со входными данными на этот счёт.

        interface New {
        }
    
        interface Exist {
        }
    
        interface UpdateName extends Exist {
        }
    
        interface Details {
        }
    
        interface AdminDetails {
        }

    Теперь мы можем аннотировать поля так, как нам нужно (видны все, кроме пароля):

        @Null(groups = {New.class})
        @NotNull(groups = {UpdateName.class})
        @JsonView({Details.class})
        private Long id;
    
        @NotNull(groups = {New.class, UpdateName.class})
        @JsonView({Details.class})
        private String name;
    
        @NotNull(groups = {New.class})
        @Null(groups = {UpdateName.class})
        @JsonView({Details.class})
        private String login;
    
        @NotNull(groups = {New.class})
        @Null(groups = {UpdateName.class})
        @JsonView({AdminDetails.class})
        private String password;
    
        @NotNull(groups = {New.class})
        @Null(groups = {UpdateName.class})
        @Email(groups = {New.class})
        @JsonView({Details.class})
        private String email;

    Осталось пометить нужные методы контроллера:

        @JsonView(Details.class)
        @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces = 
        MediaType.APPLICATION_JSON_VALUE)
        public ResponseEntity<UserDto> create(@Validated(UserDto.New.class) @RequestBody UserDto dto) {
           return new ResponseEntity<>(service.save(dto), HttpStatus.OK);
        }
    
        @JsonView(Details.class)
        @PutMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces = 
        MediaType.APPLICATION_JSON_VALUE)
        public ResponseEntity<UserDto> updateName(@Validated(UserDto.UpdateName.class) @RequestBody UserDto dto) {
           return new ResponseEntity<>(service.update(dto), HttpStatus.OK);
        }

    А когда-нибудь в другой раз мы пометим аннотацией @JsonView(AdminDetails.class) метод, который будет дёргать только пароль. Если же мы хотим, чтобы админ получал всю информацию, а не только пароль, аннотируем соответствующим образом все нужные поля:

        @Null(groups = {New.class})
        @NotNull(groups = {UpdateName.class})
        @JsonView({Details.class, AdminDetails.class})
        private Long id;
    
        @NotNull(groups = {New.class, UpdateName.class})
        @JsonView({Details.class, AdminDetails.class})
        private String name;
    
        @NotNull(groups = {New.class})
        @Null(groups = {UpdateName.class})
        @JsonView({Details.class, AdminDetails.class})
        private String login;
    
        @NotNull(groups = {New.class})
        @Null(groups = {UpdateName.class})
        @JsonView({AdminDetails.class})
        private String password;
    
        @NotNull(groups = {New.class})
        @Null(groups = {UpdateName.class})
        @Email(groups = {New.class})
        @JsonView({Details.class, AdminDetails.class})
        private String email;

    Надеюсь, эта статья помогла разобраться с валидацией входных DTO и видимостью данных выходных.
    Share post
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 22

      +1
      опускаю [...] геттеры-сеттеры — уверен, вы умеете их создавать

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


      А почему геттеры-сеттеры, а не public final? DTO ведь по своей сути просто иммутабельная структура.

        0
        Так сложилось исторически ru.wikipedia.org/wiki/JavaBeans
          +1

          В принципе, Вы правы, суть DTO заключается в простой передаче объекта, но в Java всё-таки лучше придерживаться спецификации Java Beans. Для DTO стандартным будет набор "конструктор со всеми полями + геттеры".

            0

            Также, допустим, Model Mapper именно сетит поля, поэтому, при его использовании нужны ещё и сеттеры.

              0
              Хотя бы для того, чтобы можно было поменять значение поле и сохранить тот же объект в базу, а не создавать новый этой операции.
                +1

                Сохранить DTO в базу? Зачем?

              0
              Жуть какая. Можно было просто сделать 2 разных DTO. Без всяких магических аннотаций.
                0
                Правильно, зачем вообще лезть в этот жуткий Spring?
                0

                А в каком пакете лежит аннотация Validation?

                  0
                  Для пользования нужно подключить Spring Context.
                    0

                    Вы не путаете с @Validated?

                      0

                      Да, опечатка. Исправлю. Писал не в IDE.

                  0
                  Я опускаю конструкторы и геттеры-сеттеры — уверен, вы умеете их создавать, а увеличивать в 3-4 раза код смысла не вижу — представим, что они уже есть.

                  Для этого придумали lombok и аннотацию Data
                    0
                    А какие сообщения об ошибках будут уходить в ответе? Хотелось бы взглянуть
                      0

                      Выбросит HttpStatus 409 и не пропустит дальше аннотации.

                        0
                        Плюсую. Также интересуют более сложные варианты валидации. К примеру, если ли пользователь с таким именем уже в БД, валидация завязанная на несколько полей сразу и проч. Будет ли продолжение?
                        P.S. Также не нашёл аннотацию @Validation. Есть @Validated. Возможно перепутали?
                          0
                          Да, опечатался. Исправил.
                            0
                            Не претендую на правоту, но «более сложные варианты валидации», наверное, все же лучше реализовывать махровой императивщиной, а не магическими аннотациями — чем сложнее будет маршрут валидации, тем сложнее будет понять, что, как и в каком порядке проверяется. Плюс это будет дебажится, что позволит найти ответ на рано или поздно возникнувший вопрос «а почему конкретно в этом случае не работает?»
                              0
                              Согласен. Тем не менее, интересуют варианты реализации, общий каркас, скажем так.
                                0
                                Можно как-то сделать чтобы часть валидировались аннотациями а часть махровой императивщиной?
                            0
                            JSR-303 неплох, но в сложных случаях декларативная логика так или иначе пасует, и то что по идее должно было упрощать код, вырождается в помойку из аннотаций. Неоднократно такое видел. Вот есть стандартный бин, и над каждым полем пачка аннотаций из 10+ строк JPA, JSR-303, Jackson, JAXB, Lombok… Хочется их уже просто в отдельный файл убирать или типа того.

                          Only users with full accounts can post comments. Log in, please.