Skip to main content

A guide to input validation with Spring Boot

Escrito por:
Lucien Chemaly
Lucien Chemaly
feature-bean-validation

12 de setembro de 2023

0 minutos de leitura
{"id":1,"name":"John Doe Updated","email":"john.doe.updated@example.com","password":"newP@ssW0rd!"}

If you're a developer working with Java, you likely know Spring Boot, the robust framework that streamlines the development of stand-alone, production-grade, Spring-based applications. One of the many features it offers is Bean Validation, which is a crucial aspect of any application to ensure data integrity and improve user experience.

Bean Validation can be used in Spring Boot applications for input and form validation, data validation prior to saving to a database, and enforcement of various security policies. With Bean Validation, developers can prevent errors, improve the overall quality of the application, and ensure that data is consistent throughout the application.

In this article, you'll learn more about the various applications of Bean Validation, including how to implement it in Spring Boot, enabling you to effectively utilize it for your own projects.

What is Bean Validation

Bean Validation is a feature that allows you to apply constraints to your application's data models to ensure that the data adheres to specific rules before it's processed or stored. This is achieved through the integration of the Jakarta Bean Validation, which provides a set of annotations and interfaces to define and validate constraints.

By applying Bean Validation, developers can save time and resources by catching errors and inconsistencies early on in the development process. In addition, Bean Validation helps improve the overall user experience by reducing the amount of incorrect data that's entered.

Let's take a look at some common use cases of Bean Validation in application development:

Input and form validation

One of the most common use cases of Bean Validation is input and form validation. This helps ensure that users input data in the correct format (i.e. sign-up form validation with email addresses, usernames, and passwords).

Data validation prior to saving to a database

Another application of Bean Validation is data validation before saving it to a database. This technique helps ensure that the data entered is consistent and accurate. In turn, this helps prevent errors and reduces the risk of data corruption.

Enforcement of security policies

Bean Validation can also be used for enforcing security policies by validating user input, or as data mapped from the database, to ensure that it meets specific criteria, such as a password strength requirement. Validation plays a key role in preventing injection attacks and helps prevent security vulnerabilities such as stored cross-site scripting (XSS).

Business logic validation

Bean Validation can be applied to validate business logic and ensure it meets specific requirements or standards. By defining validation constraints, such as password complexity or valid order item quantities and prices, Bean Validation can automatically check data and enforce the required rules. This helps to eliminate manual checking and ensures the application adheres to specific standards or requirements.

Implementing Spring Boot Bean Validation

In this tutorial, you will implement Bean Validation for a simple create, read, update, delete (CRUD) application with an in-memory database. Users provide their name, email, and password, which must meet specific criteria to be accepted. The high-level architecture used here consists of a backend server with RESTful APIs and an in-memory H2 database.

When a client application calls the web service (which is the application of this tutorial), the request goes to the UserController. After that, the controller applies data validation to the request. If the data is valid, the request proceeds to the UserRepository to communicate with the in-memory database and return the relevant response. Otherwise, the data is not valid and the request is sent back to the client with an error message:

blog-bean-validation-flow

Prerequisites

Before diving into the implementation, ensure you have the following tools and technologies installed:

Create a new Spring Boot project

To create a new Spring Boot project, visit the Spring Initializr and choose the following options:

  • Project type: Maven Project

  • Language: Java

  • Packaging: Jar

  • Java version: 17

  • Spring Boot: 3.1.0 (SNAPSHOT)

Enter the following details in the Project Metadata section:

  • Group: com.example

  • Artifact: simple-crud-bean-validation

  • Name: simple-crud-bean-validation

  • Description: A simple CRUD application with Spring Boot

  • Package name: com.example.simplecrud

And use the following dependencies:

  • Web: Spring Web

  • Validation: Bean Validation

  • H2 Database: H2 Database

  • Spring Data JPA: Spring Data and Hibernate

blog-bean-validation-initializr

Click Generate to download the project as a ZIP file. Then, extract the ZIP file and import the project into your preferred IDE.

Implement the application

We'll be following the standard project structure for Spring Boot applications. Here's the complete project structure:

blog-bean-validation-structure

User model

To implement the application, you need to start by creating the model directory inside src/main/java/com/example/simplecrudbeanvalidation. Then, inside the model directory, create a file named User.java and add the following fields and Bean Validation annotations to the User class:

1import jakarta.persistence.*;
2import jakarta.validation.constraints.*;
3
4@Entity
5@Table(name="users")
6public class User {
7
8    @Id
9    @GeneratedValue(strategy = GenerationType.IDENTITY)
10    private Long id;
11
12    @NotNull
13    @NotEmpty
14    @Pattern(regexp = "[a-zA-Z0-9 ]")
15    private String name;
16
17    @NotNull
18    @NotEmpty
19    @Email
20    @Pattern(regexp=".+@.+\\..+")
21    private String email;
22
23    @NotNull
24    @NotEmpty
25    @Size(min = 8, max = 64)
26    private String password;
27
28    public Long getId() {
29        return id;
30    }
31
32    public void setId(Long id) {
33        this.id = id;
34    }
35
36    public String getName() {
37        return name;
38    }
39
40    public void setName(String name) {
41        this.name = name;
42    }
43
44    public String getEmail() {
45        return email;
46    }
47
48    public void setEmail(String email) {
49        this.email = email;
50    }
51
52    public String getPassword() {
53        return password;
54    }
55
56    public void setPassword(String password) {
57        this.password = password;
58    }
59
60    public User(Long id, String name, String email, String password){
61        this.id = id;
62        this.name = name;
63        this.email = email;
64        this.password = password;
65    }
66
67    //default constructor
68    public User(){}
69}

This code defines a Java class named User that represents a table in a database called users using Jakarta Persistence API (JPA) annotations. It has four instance variables — id, name, email, and password — with corresponding getters and setters. Each of these fields has certain validation constraints, such as @NotNull, @NotEmpty, @Email, @Size, and @Pattern. @Pattern is used to indicate that the password must have eight to sixty-four characters and contain at least one digit, one lowercase letter, one uppercase letter, and one special character. Additionally, the @Pattern is also used for the name property to prevent any script injections. The class can be used as a model class to store and retrieve user data from a database.

User repository

To handle CRUD operations using an in-memory database, create the repository directory inside src/main/java/com/example/simplecrudbeanvalidation. Then, inside the repository directory, create a file named UserRepository.java to define the UserRepository interface, as follows:

1import com.example.simplecrudbeanvalidation.model.User;
2import org.springframework.data.repository.CrudRepository;
3import org.springframework.stereotype.Repository;
4
5@Repository
6public interface UserRepository extends CrudRepository<User, Long> {
7}

This code defines a Java interface called UserRepository that extends the CrudRepository interface provided by the Spring Data JPA. The UserRepository interface specifies two generic parameters for the CrudRepository: User, which is the entity type this repository manages, and Long, which is the entity's primary key type.

By extending CrudRepository, UserRepository inherits several methods for performing CRUD operations on the User entity. These methods can be used to interact with an in-memory database to manage User objects without the need for complex configuration or a full-fledged database server.

User controller

To create the UserController class that defines the RESTful API endpoints and validation logic, start by creating a directory named controller inside the src/main/java/com/example/simplecrudbeanvalidation directory. Inside the controller directory, create a file named UserController.java with the following content:

1import com.example.simplecrudbeanvalidation.model.User;
2import com.example.simplecrudbeanvalidation.repository.UserRepository;
3import jakarta.validation.Valid;
4import org.springframework.beans.factory.annotation.Autowired;
5import org.springframework.http.ResponseEntity;
6import org.springframework.validation.annotation.Validated;
7import org.springframework.web.bind.annotation.*;
8import java.util.List;
9import java.util.Optional;
10
11@RestController
12@RequestMapping("/api/users")
13@Validated
14public class UserController
15{
16   @Autowired
17   private UserRepository userRepository;
18
19   @GetMapping
20   public List<User> getAllUsers() {
21       return (List<User>) userRepository.findAll();
22   }
23
24   @GetMapping("/{id}")
25   public ResponseEntity<User> getUserById(@PathVariable Long id) {
26       Optional<User> userOptional = userRepository.findById(id);
27       if (userOptional.isPresent()) {
28           return ResponseEntity.ok(userOptional.get());
29       } else {
30           return ResponseEntity.notFound().build();
31       }
32   }
33
34   @PostMapping
35   public User createUser(@Valid @RequestBody User user) {
36       return userRepository.save(user);
37   }
38
39   @PutMapping("/{id}")
40   public ResponseEntity<User> updateUser(@PathVariable Long id, @Valid @RequestBody User updatedUser) {
41       Optional<User> userOptional = userRepository.findById(id);
42       if (userOptional.isPresent()) {
43           User user = userOptional.get();
44           user.setName(updatedUser.getName());
45           user.setEmail(updatedUser.getEmail());
46           user.setPassword(updatedUser.getPassword());
47           userRepository.save(user);
48           return ResponseEntity.ok(user);
49       } else {
50           return ResponseEntity.notFound().build();
51       }
52   }
53
54   @DeleteMapping("/{id}")
55   public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
56       if (userRepository.existsById(id)) {
57           userRepository.deleteById(id);
58           return ResponseEntity.ok().build();
59       } else {
60           return ResponseEntity.notFound().build();
61       }
62   }
63}

This code defines a Spring Boot RestController class called UserController that handles HTTP requests for managing User objects. The class contains five methods that handle different HTTP requests: retrieving all users, retrieving a single user by ID, creating a new user, updating an existing user, and deleting a user.

The @Autowired annotation is used to inject the UserRepository dependency into the UserController class. The UserController class is associated with the /api/users endpoint through the @RequestMapping annotation, which maps incoming HTTP requests to this controller. In addition, the @Validated annotation is used to enable the validation of request parameters. Overall, this code provides a RESTful API for CRUD operations on User objects using Spring Data and an in-memory database.

Configure the In-Memory Database

To configure the in-memory database, go to the application.properties under the src/main/resources/ and add the following configurations:

spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect

This Spring Boot configuration sets up an H2 in-memory database named testdb and configures its driver, username, and password. It also specifies the H2 dialect that should be used for Hibernate-based database operations.

H2 not recommended for production

The use of H2 as a production database is not recommended, and the default username and password should not be used in a production environment. While this setup is suitable for demo purposes, it should not be used in production.

Unit Test for Bean Validation

Once you've configured the in-memory database, you can begin unit testing for Bean Validation. To do so, create the controller directory inside src/test/java/com/example/simplecrudbeanvalidation. Then, inside the controller directory, create a file named UserControllerTest.java and write tests to verify that the Bean Validation constraints are working as expected.

In the UserControllerTest class, add the following code:

1import com.example.simplecrudbeanvalidation.model.User;
2import com.example.simplecrudbeanvalidation.repository.UserRepository;
3import com.fasterxml.jackson.databind.ObjectMapper;
4import org.junit.jupiter.api.Test;
5import org.springframework.beans.factory.annotation.Autowired;
6import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
7import org.springframework.boot.test.context.SpringBootTest;
8import org.springframework.boot.test.mock.mockito.MockBean;
9import org.springframework.http.MediaType;
10import org.springframework.test.web.servlet.MockMvc;
11import java.util.Arrays;
12import java.util.Optional;
13import static org.hamcrest.Matchers.hasSize;
14import static org.mockito.Mockito.*;
15import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
16import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
17
18@SpringBootTest
19@AutoConfigureMockMvc
20public class UserControllerTest {
21
22    @Autowired
23    private MockMvc mockMvc;
24
25    @MockBean
26    private UserRepository userRepository;
27
28    @Autowired
29    private ObjectMapper objectMapper;
30
31    @Test
32    public void createUser_validData_success() throws Exception {
33        User newUser = new User(null, "John Doe", "john.doe@example.com", "p@ssW0rd!");
34        User savedUser = new User(1L, newUser.getName(), newUser.getEmail(), newUser.getPassword());
35
36        when(userRepository.save(any(User.class))).thenReturn(savedUser);
37
38        mockMvc.perform(post("/api/users")
39                        .contentType(MediaType.APPLICATION_JSON)
40                        .content(objectMapper.writeValueAsString(newUser)))
41                .andExpect(status().isOk())
42                .andExpect(jsonPath("$.id").value(savedUser.getId()))
43                .andExpect(jsonPath("$.name").value(savedUser.getName()))
44                .andExpect(jsonPath("$.email").value(savedUser.getEmail()))
45                .andExpect(jsonPath("$.password").value(savedUser.getPassword()));
46    }
47
48    @Test
49    public void createUser_invalidData_failure() throws Exception {
50        User invalidUser = new User(null, "", "invalid-email", "123");
51
52        mockMvc.perform(post("/api/users")
53                        .contentType(MediaType.APPLICATION_JSON)
54                        .content(objectMapper.writeValueAsString(invalidUser)))
55                .andExpect(status().isBadRequest());
56    }
57
58    @Test
59    public void getAllUsers() throws Exception {
60        User user1 = new User(1L, "John Doe", "john.doe@example.com", "p@ssW0rd!");
61        User user2 = new User(2L, "Jane Doe", "jane.doe@example.com", "p@ssW0rd!");
62        when(userRepository.findAll()).thenReturn(Arrays.asList(user1, user2));
63        mockMvc.perform(get("/api/users"))
64                .andExpect(status().isOk())
65                .andExpect(jsonPath("$", hasSize(2)))
66                .andExpect(jsonPath("$[0].id").value(user1.getId()))
67                .andExpect(jsonPath("$[0].name").value(user1.getName()))
68                .andExpect(jsonPath("$[0].email").value(user1.getEmail()))
69                .andExpect(jsonPath("$[0].password").value(user1.getPassword()))
70                .andExpect(jsonPath("$[1].id").value(user2.getId()))
71                .andExpect(jsonPath("$[1].name").value(user2.getName()))
72                .andExpect(jsonPath("$[1].email").value(user2.getEmail()))
73                .andExpect(jsonPath("$[1].password").value(user2.getPassword()));
74    }
75
76    @Test
77    public void getUserById_found() throws Exception {
78        User user = new User(1L, "John Doe", "john.doe@example.com", "p@ssW0rd!");
79        when(userRepository.findById(user.getId())).thenReturn(Optional.of(user));
80        mockMvc.perform(get("/api/users/{id}", user.getId()))
81                .andExpect(status().isOk())
82                .andExpect(jsonPath("$.id").value(user.getId()))
83                .andExpect(jsonPath("$.name").value(user.getName()))
84                .andExpect(jsonPath("$.email").value(user.getEmail()))
85                .andExpect(jsonPath("$.password").value(user.getPassword()));
86    }
87
88    @Test
89    public void getUserById_notFound() throws Exception {
90        Long userId = 1L;
91        when(userRepository.findById(userId)).thenReturn(Optional.empty());
92        mockMvc.perform(get("/api/users/{id}", userId))
93                .andExpect(status().isNotFound());
94    }
95}

This code is a set of JUnit tests for the UserController class. The tests use the MockMvc class to simulate HTTP requests to the /api/users endpoint and verify that the corresponding methods in the UserController class handle them correctly. The tests cover scenarios such as creating a new user with valid and invalid data, retrieving all users, and retrieving a user by ID when the user exists and when it does not.

The @SpringBootTest annotation is used to load the application context, and the @MockBean annotation is used to mock the UserRepository dependency of the UserController class. Overall, these tests ensure that the UserController class functions correctly and responds appropriately to various HTTP requests.

To run the tests, open a terminal or shell, navigate to the root directory of your project, and run the following command:

mvn test

You should see that all your tests have passed:

…output omitted…
[INFO] Results:
[INFO] 
[INFO] Tests run: 5, Failures: 0, Errors: 0, Skipped: 0
[INFO] 
[INFO] ------------------------------------------------------------------------
…output omitted…

Run and test your APIs

In IntelliJ, right-click your SimpleCrudBeanValidationApplication.java file and select Run 'SimpleCrudBeanValidationApplication.main()':

blog-bean-validation-run

Alternatively, you can run your application in the terminal or shell by using the following commands:

mvn clean install
java -jar target/simple-crud-bean-validation-0.0.1-SNAPSHOT.jar

After starting the Spring Boot application, the CRUD operations can be tested using curl commands from the terminal or shell, which is similar to how a frontend application or Postman would call these APIs.

To test the CRUD operations, begin by creating a user:

curl -X POST -H "Content-Type: application/json" -d '{"name":"John Doe","email":"john.doe@example.com","password":"p@ssW0rd!"}' http://localhost:8080/api/users

Your output looks like this:

{"id":1,"name":"John Doe","email":"john.doe@example.com","password":"p@ssW0rd!"} 

Then get all your users:

curl -X GET http://localhost:8080/api/users

Your output looks like this:

{"id":1,"name":"John Doe","email":"john.doe@example.com","password":"p@ssW0rd!"}

Next, get your user by ID. Replace <id> with the ID of the user you want to retrieve:

curl -X GET http://localhost:8080/api/users/<id>

The following is the output:

{"id":1,"name":"John Doe","email":"john.doe@example.com","password":"p@ssW0rd!"}

Update the user by replacing <id> with the ID of the user you want to update:

curl -X PUT -H "Content-Type: application/json" -d '{"name":"John Doe Updated","email":"john.doe.updated@example.com","password":"newP@ssW0rd!"}' http://localhost:8080/api/users/<id>

Your output looks like this:

{"id":1,"name":"John Doe Updated","email":"john.doe.updated@example.com","password":"newP@ssW0rd!"}

Then, delete a user by replacing <id> with the ID of the user you want to delete:

curl -X DELETE http://localhost:8080/api/users/<id>

The output of this command is empty.

Using these curl commands, you can test the CRUD operations of your User RESTful API. Remember to replace <id> with the appropriate user ID in each command. By following these steps, you can interact with your Spring Boot application and verify that the Bean Validation constraints are functioning as expected during runtime.

If you attempt to run these curl requests again with incorrect entries, such as an invalid email, password, or empty values, you will find that the API returns a bad request error. This error message indicates that the validation performed on the controller level, also known as Bean Validation, did not pass.

Let's look at some examples of bad requests. Here, you create a user with an invalid email address:

curl -X POST -H "Content-Type: application/json" -d '{"name":"John Doe","email":"johexample","password":"p@ssW0rd!"}' http://localhost:8080/api/users

Your output looks like this:

{"timestamp":"2023-03-28T10:27:21.884+00:00","status":400,"error":"Bad Request","path":"/api/users"}

And here, you create a user with an invalid password:

curl -X POST -H "Content-Type: application/json" -d '{"name":"John Doe","email":"joh@example.com","password":"test"}' http://localhost:8080/api/users

The following is the output:

{"timestamp":"2023-03-28T10:29:30.793+00:00","status":400,"error":"Bad Request","path":"/api/users"}

Concluding our validation

In this article, you learned how to implement Spring Boot Bean Validation in a simple CRUD application. You created a project using Spring Initializr, added necessary dependencies, and implemented a user model with Bean Validation annotations. You also implemented a UserRepository and a UserController with RESTful API endpoints that utilize Bean Validation and wrote tests to verify that the Bean Validation constraints are working as expected.

To further improve the security of your application, use Snyk Code, a SAST that detects insecure code patterns that can lead to injection attacks. You can use the Snyk Security plugin for IntelliJ IDE extension to integrate this analysis into your development workflow.

Now that you understand how Spring Boot Bean Validation works, you can implement it in your own projects, ensuring data integrity and improving user experience. To explore the completed code, please visit the following GitHub repository.