Skip to main content

Spring REST Validation

In this tutorial, we'll explore how to implement validation in Spring REST APIs. Validation is an essential aspect of building robust applications as it helps ensure that data received by your application meets expected criteria before processing.

Introduction to REST Validation

When building REST APIs, you need to validate incoming data to:

  • Prevent bad data from entering your application
  • Provide clear error messages to clients
  • Reduce the need for complex error handling in your business logic
  • Improve security by rejecting potentially malicious inputs

Spring offers seamless integration with Bean Validation API (JSR-380) through Hibernate Validator, the reference implementation of this specification.

Setting Up Validation Dependencies

To use validation in your Spring Boot application, you need to include the Spring Boot starter validation dependency:

Maven

xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

Gradle

gradle
implementation 'org.springframework.boot:spring-boot-starter-validation'

Basic Validation Annotations

Spring REST validation uses annotations to define constraints. Here are some common validation annotations:

AnnotationDescription
@NotNullValidates that the value is not null
@NotEmptyValidates that the value is not null or empty
@NotBlankValidates that the string is not null and contains at least one non-whitespace character
@SizeValidates that the size of a collection, array, or string is within a specified range
@MinValidates that the number is greater than or equal to a specified value
@MaxValidates that the number is less than or equal to a specified value
@EmailValidates that the string is a valid email address
@PatternValidates that the string matches a regular expression

Implementing Validation in REST Controllers

Let's implement validation in a Spring REST controller using a practical example:

1. Create a Model Class with Validation Annotations

First, let's create a User class with validation rules:

java
package com.example.validation.model;

import jakarta.validation.constraints.*;
import java.time.LocalDate;

public class User {

@NotNull(message = "ID cannot be null")
private Long id;

@NotBlank(message = "Name is required")
@Size(min = 2, max = 50, message = "Name must be between 2 and 50 characters")
private String name;

@NotBlank(message = "Email is required")
@Email(message = "Email must be valid")
private String email;

@NotNull(message = "Date of birth is required")
@Past(message = "Date of birth must be in the past")
private LocalDate dateOfBirth;

@Min(value = 18, message = "Age must be at least 18")
private int age;

// Getters and Setters
// Constructor(s)
}

2. Create a REST Controller with Validation

Now, let's create a controller that validates incoming requests:

java
package com.example.validation.controller;

import com.example.validation.model.User;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import jakarta.validation.Valid;
import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/api/users")
@Validated
public class UserController {

@PostMapping
public ResponseEntity<Object> createUser(@Valid @RequestBody User user) {
// In a real application, you would save the user to a database

Map<String, Object> response = new HashMap<>();
response.put("message", "User created successfully");
response.put("user", user);

return new ResponseEntity<>(response, HttpStatus.CREATED);
}

@PutMapping("/{id}")
public ResponseEntity<Object> updateUser(
@PathVariable Long id,
@Valid @RequestBody User user) {

// Check if IDs match
if (!id.equals(user.getId())) {
return new ResponseEntity<>("IDs don't match", HttpStatus.BAD_REQUEST);
}

// In a real application, you would update the user in the database

Map<String, Object> response = new HashMap<>();
response.put("message", "User updated successfully");
response.put("user", user);

return new ResponseEntity<>(response, HttpStatus.OK);
}
}

Notice the key annotations:

  • @Valid - Tells Spring to validate the annotated request body
  • @Validated - Activates method parameter validation for the controller class

Handling Validation Errors

When validation fails, Spring throws MethodArgumentNotValidException. Let's create a global exception handler to provide user-friendly error responses:

java
package com.example.validation.exception;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

import java.util.HashMap;
import java.util.Map;

@ControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Object> handleValidationExceptions(
MethodArgumentNotValidException ex) {

Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getAllErrors().forEach((error) -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});

Map<String, Object> response = new HashMap<>();
response.put("message", "Validation failed");
response.put("errors", errors);

return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
}
}

Testing Our Validation

Now, let's test our API with valid and invalid data:

Invalid Request Example

Request:

http
POST /api/users HTTP/1.1
Host: localhost:8080
Content-Type: application/json

{
"id": 1,
"name": "J",
"email": "invalid-email",
"dateOfBirth": "2025-01-01",
"age": 15
}

Response:

json
{
"message": "Validation failed",
"errors": {
"name": "Name must be between 2 and 50 characters",
"email": "Email must be valid",
"dateOfBirth": "Date of birth must be in the past",
"age": "Age must be at least 18"
}
}

Valid Request Example

Request:

http
POST /api/users HTTP/1.1
Host: localhost:8080
Content-Type: application/json

{
"id": 1,
"name": "John Smith",
"email": "[email protected]",
"dateOfBirth": "1990-01-01",
"age": 33
}

Response:

json
{
"message": "User created successfully",
"user": {
"id": 1,
"name": "John Smith",
"email": "[email protected]",
"dateOfBirth": "1990-01-01",
"age": 33
}
}

Advanced Validation Techniques

Group Validation

Sometimes you need different validation rules for different operations (create vs. update). You can use validation groups:

First, define interfaces for the groups:

java
package com.example.validation.validation;

public interface ValidationGroups {
interface Create {}
interface Update {}
}

Then specify the groups in your model:

java
package com.example.validation.model;

import com.example.validation.validation.ValidationGroups.Create;
import com.example.validation.validation.ValidationGroups.Update;
import jakarta.validation.constraints.*;
import java.time.LocalDate;

public class User {

@Null(groups = Create.class, message = "ID must be null for creation")
@NotNull(groups = Update.class, message = "ID cannot be null for update")
private Long id;

@NotBlank(message = "Name is required")
@Size(min = 2, max = 50, message = "Name must be between 2 and 50 characters")
private String name;

// Other fields and methods remain the same
}

Finally, specify the group in your controller:

java
@PostMapping
public ResponseEntity<Object> createUser(
@Validated(Create.class) @RequestBody User user) {
// Implementation
}

@PutMapping("/{id}")
public ResponseEntity<Object> updateUser(
@PathVariable Long id,
@Validated(Update.class) @RequestBody User user) {
// Implementation
}

Custom Validation

You can also create custom validation annotations:

  1. Create a validation annotation:
java
package com.example.validation.validation;

import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.*;

@Documented
@Constraint(validatedBy = PasswordConstraintValidator.class)
@Target({ ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidPassword {
String message() default "Invalid password";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
  1. Implement the validator:
java
package com.example.validation.validation;

import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;

public class PasswordConstraintValidator implements ConstraintValidator<ValidPassword, String> {

@Override
public void initialize(ValidPassword constraintAnnotation) {
}

@Override
public boolean isValid(String password, ConstraintValidatorContext context) {
// Password must be at least 8 characters
// Must contain at least one digit
// Must contain at least one uppercase letter
if (password == null) {
return false;
}

boolean hasLength = password.length() >= 8;
boolean hasDigit = password.matches(".*\\d.*");
boolean hasUppercase = !password.equals(password.toLowerCase());

boolean isValid = hasLength && hasDigit && hasUppercase;

if (!isValid) {
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(
"Password must be at least 8 characters, contain a digit and an uppercase letter"
).addConstraintViolation();
}

return isValid;
}
}
  1. Use the annotation in your model:
java
@ValidPassword
private String password;

Parameter Validation

You can also validate path variables and request parameters:

java
@GetMapping("/{id}")
public ResponseEntity<Object> getUser(
@PathVariable @Min(1) Long id) {
// Implementation
}

@GetMapping("/search")
public ResponseEntity<Object> searchUsers(
@RequestParam @NotBlank String keyword) {
// Implementation
}

Best Practices for REST Validation

  1. Keep validation logic separate from business logic: Use validation annotations for simple validation and custom validators for complex rules.

  2. Provide clear error messages: Always include descriptive messages in validation annotations to help clients understand the issue.

  3. Use appropriate HTTP status codes: Use 400 Bad Request for validation errors and provide detailed error information in the response body.

  4. Validate at the controller level: Apply validation at the entry point of your application (controllers).

  5. Use validation groups: When different operations require different validation rules.

  6. Don't trust client-side validation: Always validate on the server side, even if validation is also done on the client.

Summary

In this tutorial, we've covered:

  • Setting up Spring REST validation
  • Using basic validation annotations
  • Implementing validation in REST controllers
  • Handling validation errors
  • Advanced validation techniques including group validation and custom validators
  • Best practices for REST validation

Properly implemented validation improves the quality of your API, reduces bugs, and provides better user experience by providing clear feedback when something goes wrong.

Additional Resources

Exercises

  1. Create a Product class with appropriate validation for fields like name, price, and category.
  2. Implement a REST controller for product management with validation.
  3. Add custom validation to ensure product codes follow a specific pattern.
  4. Implement group validation to handle different validation rules for product creation versus updates.
  5. Create a global exception handler that returns validation errors in a standardized format.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)