Pull to refresh

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

Reading time 6 min
Views 46K
Всем привет! Сегодня мы коснёмся валидации данных, входящих через 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 и видимостью данных выходных.
Tags:
Hubs:
+8
Comments 22
Comments Comments 22

Articles