# Bean Validation

Bean Validation in Spring Boot is a framework for validating the properties of a JavaBean using annotations. It leverages the Java Bean Validation API (JSR 380) and Hibernate Validator  to perform validation. It

* Ensure data integrity by validating user input at the application layer.
* Reduce boilerplate validation logic in your code by relying on declarative annotations.
* Integrates seamlessly with Spring Boot's controller and service layers.
* To use bean validation, use starter

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

### **Common Bean Validation Annotations**

| **Annotation**            | **Description**                                                                 |
| ------------------------- | ------------------------------------------------------------------------------- |
| `@NotNull`                | Ensures the property is not `null`.                                             |
| `@NotEmpty`               | Ensures the property is not `null` or empty (only for Strings and collections). |
| `@NotBlank`               | Ensures the property is not `null` or blank (for Strings).                      |
| `@Size`                   | Restricts the size of a String, collection, array, or map.                      |
| `@Min` / `@Max`           | Sets minimum and maximum values for numeric fields.                             |
| `@Email`                  | Validates an email format.                                                      |
| `@Pattern`                | Validates against a regular expression.                                         |
| `@Positive` / `@Negative` | Ensures the property is positive or negative.                                   |
| `@Valid`                  | Triggers validation for nested objects.                                         |
| `@Past` / `@Future`       | Ensures the property is a date in the past or future.                           |

{% hint style="info" %}
To validate bean object we can use **@Valid** in controller or service layer&#x20;
{% endhint %}

### How to handle exceptions when validation fails?

```java
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidationExceptions(MethodArgumentNotValidException ex) {
    Map<String, String> errors = new HashMap<>();
    ex.getBindingResult().getFieldErrors().forEach(error -> 
        errors.put(error.getField(), error.getDefaultMessage())
    );
    return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
}
```

### How to validate nested objects?

Use `@Valid` on nested objects to trigger validation:

```java
public class Order {
    @Valid
    @NotNull
    private User user;
}
```

### How to do groups and conditional validation?

To do group or conditional validation, we can use **groups** attribute and **@Validated .**

```java
// first create groups
public interface CreateGroup {}
public interface UpdateGroup {}

// attach group with groups attrubute
public class User {

    @NotNull(groups = CreateGroup.class)
    private String name;

    @NotNull(groups = UpdateGroup.class)
    private Integer id;
}

// use @validated with specific group that want to validate
@PostMapping
public ResponseEntity<String> createUser(@Validated(CreateGroup.class) @RequestBody User user) {
   return ResponseEntity.ok("User created");
}

```

### How to do custom validation of beans?

To do custom validation of beans we can use validation factory and the validator class&#x20;

```java

public class BeanValidator {

    private static final ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
    private static final Validator validator = factory.getValidator();

    public static <T> void validate(T bean) {
        Set<ConstraintViolation<T>> violations = validator.validate(bean);
        if (!violations.isEmpty()) {
            StringBuilder sb = new StringBuilder();
            for (ConstraintViolation<T> violation : violations) {
                sb.append(violation.getPropertyPath()).append(": ").append(violation.getMessage()).append("\n");
            }
            throw new IllegalArgumentException("Bean validation failed:\n" + sb.toString());
        }
    }
}
```

### How to create own custom validation constraints?

&#x20;To create our own custom validation constraints we need to create a custom annotation a validator  class by extending **ConstraintValidator.** &#x20;

Below there is an example of custom constraint for validate file.

```java
// custom validation annotation

@Documented
@Constraint(validatedBy = FileValidator.class)
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidFile {
    String message() default "Invalid file(s)";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
    long maxSizeInMb() default 5 ; // 5MB default
    String[] allowedExtensions() default {"jpg", "jpeg", "png", "pdf"};
}
```

A custom validator to validate it.

```java

import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import org.springframework.web.multipart.MultipartFile;

import java.util.Arrays;
import java.util.List;

/**
 * Validator class for @ValidFile annotation, used to validate file size and extension
 * @see ValidFile
 * @Author saurabh vaish
 * @Date 04-09-2024
 */
public class FileValidator implements ConstraintValidator<ValidFile, Object> {

    private long maxSizeBytes;
    private List<String> allowedExtensions;

    @Override
    public void initialize(ValidFile constraintAnnotation) {
        this.maxSizeBytes = constraintAnnotation.maxSizeInMb() * 1024 * 1024;
        this.allowedExtensions = Arrays.asList(constraintAnnotation.allowedExtensions());
    }

    @Override
    public boolean isValid(Object value, ConstraintValidatorContext context) {
        return switch (value) {
            case null -> true; // null values are validated with @NotNull

            case MultipartFile multipartFile -> validateFile(multipartFile, context);
            case MultipartFile[] multipartFiles -> validateFiles(List.of(multipartFiles), context);
            case List<?> list -> validateFiles((List<MultipartFile>) list, context);
            default -> false;
        };

    }

    private boolean validateFile(MultipartFile file, ConstraintValidatorContext context) {
        if (file.isEmpty()) {
            return true; // Empty files are allowed, use @NotNull if you want to enforce file presence
        }

        String fileName = file.getOriginalFilename();
        if (fileName == null || fileName.contains("..")) {
            addConstraintViolation(context, "Invalid file name");
            return false;
        }

        String extension = getFileExtension(fileName);
        if (!allowedExtensions.contains(extension.toLowerCase())) {
            addConstraintViolation(context, "Invalid file type. Allowed types are: " + String.join(", ", allowedExtensions));
            return false;
        }

        if (file.getSize() > maxSizeBytes) {
            addConstraintViolation(context, "File size exceeds the maximum allowed size of " + (maxSizeBytes / 1024 / 1024) + "MB");
            return false;
        }

        return true;
    }

    private boolean validateFiles(List<MultipartFile> files, ConstraintValidatorContext context) {
        for (MultipartFile file : files) {
            if (!validateFile(file, context)) {
                return false;
            }
        }
        return true;
    }

    private void addConstraintViolation(ConstraintValidatorContext context, String message) {
        context.disableDefaultConstraintViolation();
        context.buildConstraintViolationWithTemplate(message).addConstraintViolation();
    }

    private String getFileExtension(String fileName) {
        int dotIndex = fileName.lastIndexOf('.');
        return (dotIndex == -1) ? "" : fileName.substring(dotIndex + 1);
    }
}
```

Use of this custom constraint

```java
@NotNull(message = "Image file is required if imageUrl is not provided")
@Schema(description = "Image file (optional if imageUrl is provided)", type = "string", format = "binary")
@ValidFile(message = "Invalid file. Only JPG, JPEG, and PNG files up to 5MB are allowed.", maxSizeInMb = 5,allowedExtensions = {"jpg", "jpeg", "png"})
MultipartFile file
```

### Handle Exceptions Using Problem Detail

```java
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ProblemDetail handleValidationExceptions(MethodArgumentNotValidException ex) {
        ProblemDetail problemDetail = ProblemDetail.forStatus(HttpStatus.BAD_REQUEST);
        problemDetail.setTitle("Validation Error");
        problemDetail.setDetail("Invalid input provided");
        ex.getBindingResult().getFieldErrors().forEach(error ->
            problemDetail.setProperty(error.getField(), error.getDefaultMessage())
        );
        return problemDetail;
    }
}

// reponse
{
  "type": "about:blank",
  "title": "Validation Error",
  "status": 400,
  "detail": "Invalid input provided",
  "instance": "/api/users",
  "name": "Name must not be blank",
  "email": "Email must be a valid address"
}
```


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://wisdom.gitbook.io/gyan/core/bean-validation.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
