HelloKoding

Practical coding guides

Spring Boot REST API Validation and Globally Error Handling Tutorial with Example

In this tutorial, you will learn to implement an REST API Validation + Error Handling Example in Spring Boot

REST API validation can be implemented by using Java Bean Validation API, Hibernate Validator and Unified Expression Language. Check this tutorial if you’re new to Java Bean Validation API

Error Handling for REST in Spring can be implemented by extending ResponseEntityExceptionHandler and using @ControllerAdvice class annotation to apply globally to all controllers

What you’ll need

  • JDK 8+ or OpenJDK 8+
  • Maven 3+

Stack

  • Java Bean Validation API, Hibernate Validator and Unified Expression Language
  • Spring Boot with @RestController, @RequestMapping, @Validated, ResponseEntityExceptionHandler, @ControllerAdvice and @ExceptionHandler
  • JPA, Hibernate and HSQL
  • Lombok with @Builder

Project structure

├── src
│   └── main
│       ├── java
│       │   └── com
│       │       └── hellokoding
│       │           └── springboot
│       │               └── restful
│       │                   ├── product
│       │                   │   ├── Product.java
│       │                   │   ├── ProductAPI.java
│       │                   │   ├── ProductIDExisting.java
│       │                   │   ├── ProductIDExistingValidator.java
│       │                   │   ├── ProductRepository.java
│       │                   │   └── ProductService.java
│       │                   ├── Application.java
│       │                   ├── ExceptionHandler.java
│       │                   └── ResponseDTO.java
│       └── resources
│           └── ValidationMessages.properties
└── pom.xml

Dependencies

Include spring-boot-starter-validation into your pom.xml

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

spring-boot-starter-validation contains

  • hibernate-validator: an implementation of Java Bean Validation API
  • tomcat-embed-el: an implementation of Java Unified Expression Language (EL)

Full content of pom.xml as below

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.hellokoding.springboot</groupId>
    <artifactId>springboot-restapi-validation</artifactId>
    <version>1.0-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.5.RELEASE</version>
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
        <org.apache.maven.plugins.version>3.6.0</org.apache.maven.plugins.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>org.hsqldb</groupId>
            <artifactId>hsqldb</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

Define data model and validation constraints

Define data model

Product.java

package com.hellokoding.springboot.restful.product;

import lombok.Data;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.math.BigDecimal;

@Data
@Entity
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @ProductIDExisting
    private Long id;

    @NotNull(message = "{NotNull.name}")
    private String name;

    @Size(max = 100)
    private String description;

    @Min(1)
    private BigDecimal price;
}

@NotNull, @Size and @Min are built-in constraints

@ProductIDExisting is a custom constraint defined as below

{NotNull} is message templates, its value will be replaced at run time by the below ValidationMessages.properties

Define custom constraint

ProductIDExisting.java

package com.hellokoding.springboot.restful.product;

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

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Constraint(validatedBy = ProductIDExistingValidator.class)
public @interface ProductIDExisting {
    String message() default "{ProductIDExisting}";

    Class<?>[] groups() default { };

    Class<? extends Payload>[] payload() default { };
}

{ProductIDExisting} is a message template. Its value will be replaced at run time by the below ValidationMessages.properties

Define custom constraint validator

ProductIDExistingValidator.java

package com.hellokoding.springboot.restful.product;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.Objects;

@Component
public class ProductIDExistingValidator implements ConstraintValidator<ProductIDExisting, Long> {
    @Autowired
    private ProductService productService;

    @Override
    public boolean isValid(Long productId, ConstraintValidatorContext context) {
        return Objects.isNull(productId) || productService.findById(productId).isPresent();
    }
}

Custom the validation messages

ValidationMessages.properties

NotNull.name=Name is required
ProductIDExisting=Product ID ${validatedValue} is not existing

Define ResponseDTO, REST API and API Exception Handler

ResponseDTO.java

package com.hellokoding.springboot.restful;

import lombok.Builder;
import lombok.Data;

@Data
@Builder
public class ResponseDTO<T> {
    private String status;

    @Builder.Default
    private String message = "Success!";

    private T body;
}

ResponseDTO is defined to unify REST API response data format to client

ProductAPI.java

package com.hellokoding.springboot.restful.product;

import com.hellokoding.springboot.restful.ResponseDTO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;

@RestController
@RequestMapping("/api/v1/products")
@Validated
public class ProductAPI {
    private final ProductService productService;

    @Autowired
    public ProductAPI(ProductService productService) {
        this.productService = productService;
    }

    @GetMapping
    public ResponseEntity<ResponseDTO> findAll() {
        ResponseDTO responseDTO = ResponseDTO.builder()
            .status(HttpStatus.OK.toString())
            .body(productService.findAll()).build();

        return ResponseEntity.ok(responseDTO);
    }

    @GetMapping("/{id}")
    public ResponseEntity<ResponseDTO> findById(@PathVariable @ProductIDExisting Long id) {
        ResponseDTO responseDTO = ResponseDTO.builder()
            .status(HttpStatus.OK.toString())
            .body(productService.findById(id)).build();

        return ResponseEntity.ok(responseDTO);
    }

    @PostMapping
    public ResponseEntity<ResponseDTO> create(@RequestBody Product product) {
        ResponseDTO responseDTO = ResponseDTO.builder()
            .status(HttpStatus.CREATED.toString())
            .body(productService.save(product)).build();

        return ResponseEntity.status(HttpStatus.CREATED).body(responseDTO);
    }

    @PutMapping("/{id}")
    public ResponseEntity<ResponseDTO> update(@PathVariable Long id, @RequestBody @Valid Product product) {
        ResponseDTO responseDTO = ResponseDTO.builder()
            .status(HttpStatus.ACCEPTED.toString())
            .body(productService.save(product)).build();

        return ResponseEntity.accepted().body(responseDTO);
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<ResponseDTO> delete(@PathVariable @ProductIDExisting Long id) {
        productService.deleteById(id);

        ResponseDTO responseDTO = ResponseDTO.builder()
            .status(HttpStatus.ACCEPTED.toString()).build();

        return ResponseEntity.accepted().body(responseDTO);
    }
}

REST APIs are defined with @RestController, @RequestMapping, @GetMapping, @PostMapping, @@PutMapping and @DeleteMapping

@Valid indicates validation cascading: constraints defined on the object and its properties are be validated when the property, method parameter or method return type is validated

@Validated is a variant of @Valid indicating that a specific class is supposed to be validated at the method level. In this example, it is using to trigger validation for @ProductIDExisting method parameters

APIExceptionHandler.java

package com.hellokoding.springboot.restful;

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
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 org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

import javax.validation.ConstraintViolationException;

@Slf4j
@ControllerAdvice
public class APIExceptionHandler extends ResponseEntityExceptionHandler {
    @Override
    protected ResponseEntity handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
        log.error(ex.getMessage(), ex);

        FieldError fieldError = ex.getBindingResult().getFieldError();
        ResponseDTO responseDTO = ResponseDTO.builder()
            .status(status.toString())
            .message(fieldError.getDefaultMessage()).build();

        return ResponseEntity.badRequest().body(responseDTO);
    }

    @ExceptionHandler(ConstraintViolationException.class)
    public final ResponseEntity<Object> handleConstraintViolationException(Exception ex, WebRequest request) {
        log.error(ex.getMessage(), ex);

        ResponseDTO responseDTO = ResponseDTO.builder()
            .status(HttpStatus.BAD_REQUEST.toString())
            .message(ex.getMessage()).build();

        return ResponseEntity.badRequest().body(responseDTO);
    }
}

ResponseEntityExceptionHandler provides centralized exception handling across all @RequestMapping methods through @ExceptionHandler methods which return ResponseEntity

@ControllerAdvice is a specialization for classes that declare @ExceptionHandler, @InitBinder, or @ModelAttribute methods to be shared across multiple @Controller classes

Run and Test

Run with Maven

Type command mvn clean spring-boot:run at your project root directory to run the application

Test with cURL

curl -i -X PUT -H "Content-Type:application/json" -d "{\"id\": 1, \"name\" : \"Hello Koding\", \"description\": \"Practical Coding Courses, Tutorials and Examples\", \"price\":1}" http://localhost:8080/api/v1/products/1

Expected output

{"status":"400 BAD_REQUEST","message":"Product ID 1 is not existing","body": null}

Source code

https://github.com/hellokoding/hellokoding-courses/tree/master/springboot-examples/springboot-restapi-validation

Follow HelloKoding