Configure DTO validation in Spring Framework

    Hello! Today we will touch on the validation of data entering through the Data Transfer Object (DTO), configure annotations and visibilities so that we receive and give only what we need.

    So, we have the UserDto DTO class, with the corresponding fields:

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

    I omit the constructors and getter-setters - I'm sure you know how to create them, but I don’t see the point of increasing 3-4 times the code - let's imagine that they already exist.

    We will accept DTO through the controller with CRUD methods. Again, I will not write all the CRUD methods - for the purity of the experiment, we will have a couple. Let it be create and updateName.

        @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces = 
        MediaType.APPLICATION_JSON_VALUE)
        public ResponseEntity 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 updateName(@RequestBody UserDto dto) {
           return new ResponseEntity<>(service.update(dto), HttpStatus.OK);
        }

    For clarity, they also had to be simplified. Thus, we get some kind of JSON, which is converted to UserDto, and return UserDto, which is converted to JSON and sent to the client.

    Now I propose to get acquainted with the few validation annotations with which we will work.

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

    All annotations are available in the javax.validation.constraints library. So, we will configure our DTO in such a way as to immediately receive a validated object for further translation into essence and saving to the database. Those fields that must be filled, we will mark NotNull , we will also mark 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;
    }

    We set the validation settings for DTO - all fields except id should be filled in - it is generated in the database. Add validation to the controller:

        @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces = 
        MediaType.APPLICATION_JSON_VALUE)
        public ResponseEntity 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 updateName(@Validated @RequestBody UserDto dto) {
           return new ResponseEntity<>(service.update(dto), HttpStatus.OK);
        }

    Validation configured in this way will be suitable for creating a new user, but it will not be suitable for updating existing ones - for this we will need to get the id (which is set to null), as well as skip the login, password and email fields, since we only change the name in updateName . That is, we need to get id and name, and nothing more. And here we need visibility interfaces.

    Let's create an interface directly in the DTO class (for clarity, I recommend putting such things in a separate class, or better, in a separate package, for example, transfer). The interface will be called New, the second will be called Exist, from which we will inherit UpdateName (in the future we can inherit other visibility interfaces from Exist, we will change more than one name):

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

    Now we mark our annotations with the New interface.

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

    Now these annotations only work when specifying the New interface. We can only set annotations for the case when we need to update the name field (recall, we need to specify non-zero id and name, the rest zero). Here's what it looks like:

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

    Now we just need to set the necessary settings in the controllers, register an interface to set the validation:

        @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces = 
        MediaType.APPLICATION_JSON_VALUE)
        public ResponseEntity 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 updateName(@Validated(UserDto.UpdateName.class) @RequestBody 
    UserDto dto) {
           return new ResponseEntity<>(service.update(dto), HttpStatus.OK);
        }

    Now for each method its own set of settings will be called.

    So, we figured out how to validate the input data, now it remains to validate the output. This is done using the @JsonView annotation.

    Now in the output DTO, which we give back, contains all the fields. But suppose we never need to give a password (except in exceptional cases).

    To validate the output DTO, we add two more interfaces that will be responsible for the visibility of the output - Details (for display to users) and AdminDetails (for display only to admins). Interfaces can be inherited from each other, but for simplicity of perception now we will not do this - just an example with input data on this subject.

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

    Now we can annotate the fields as we need (everything except the password is visible):

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

    It remains to mark the necessary controller methods:

        @JsonView(Details.class)
        @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces = 
        MediaType.APPLICATION_JSON_VALUE)
        public ResponseEntity 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 updateName(@Validated(UserDto.UpdateName.class) @RequestBody UserDto dto) {
           return new ResponseEntity<>(service.update(dto), HttpStatus.OK);
        }

    And some other time, we will annotate @JsonView (AdminDetails.class) a method that will only pull the password. If we want the admin to receive all the information, and not just the password, annotate all the necessary fields accordingly:

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

    I hope this article has helped to understand the validation of input DTOs and the visibility of this output.

    Also popular now: