Tuesday 9 March 2021

Spring boot Rest: Handle Method argument not valid exception

By overriding the method 'handleMethodArgumentNotValid', you can customize the behavior of method argument invalid exceptions.

 

For example,

@ControllerAdvice(basePackages = "com.sample.app")
public class CustomErrorHandler extends ResponseEntityExceptionHandler {
	@Override
	protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex,
			HttpHeaders headers, HttpStatus status, WebRequest request) {

		List<String> errors = new ArrayList<String>();

		for (FieldError error : ex.getBindingResult().getFieldErrors()) {
			errors.add(error.getField() + " -> " + error.getDefaultMessage());
		}

		for (ObjectError error : ex.getBindingResult().getGlobalErrors()) {
			errors.add(error.getObjectName() + " -> " + error.getDefaultMessage());
		}

		ApiError apiError = new ApiError(HttpStatus.BAD_REQUEST, ex.getMessage(), errors);
		return handleExceptionInternal(ex, apiError, headers, apiError.getStatus(), request);

		// If you want to throw apiError directly, uncomment this
		// return new ResponseEntity(apiError, apiError.getStatus());
	}
}

 

I defined am employee class like below.

public class Employee {
	private int id;
	
	@NotNull(message = "Firstname can't be null")
	private String firstName;
	
	@NotNull(message = "LastName can't be null")
	private String lastName;
	....
	....
}

 

As you see the above definition, I am expecting firstName and lastName properties must not be null. If this property violates, Spring throws MethodArgumentNotValidException.

 

Follow below step-by-step procedure to build complete working application.

 

Step 1: Create new maven project 'spring-rest-custom-error-handler'.

 

Step 2: Update pom.xml with maven dependencies.

 

pom.xml

 

<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.sample.app</groupId>
	<artifactId>spring-rest-custom-error-handler</artifactId>
	<version>1</version>

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

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

		<dependency>
			<groupId>io.springfox</groupId>
			<artifactId>springfox-swagger2</artifactId>
			<version>2.9.2</version>
		</dependency>


		<dependency>
			<groupId>io.springfox</groupId>
			<artifactId>springfox-swagger-ui</artifactId>
			<version>2.9.2</version>
		</dependency>


	</dependencies>
</project>

 

Step 3: Create application.yml file under src/main/resources folder.

 

application.yml

 

server:
  port: 1234

 

Step 4: Create a package 'com.sample.app.model' and define Employee, ApiError classes.

 

Employee.java

 

package com.sample.app.model;

import javax.validation.constraints.NotNull;

public class Employee {
	private int id;
	
	@NotNull(message = "Firstname can't be null")
	private String firstName;
	
	@NotNull(message = "LastName can't be null")
	private String lastName;

	public int getId() {
		return id;
	}

	public void setId(int id) {
		this.id = id;
	}

	public String getFirstName() {
		return firstName;
	}

	public void setFirstName(String firstName) {
		this.firstName = firstName;
	}

	public String getLastName() {
		return lastName;
	}

	public void setLastName(String lastName) {
		this.lastName = lastName;
	}

}

 

ApiError.java

package com.sample.app.model;

import org.springframework.http.HttpStatus;

public class ApiError {
	private HttpStatus status;
	private String message;
	private Object errors;

	public ApiError(HttpStatus status, String message, Object errors) {
		super();
		this.status = status;
		this.message = message;
		this.errors = errors;
	}


	public HttpStatus getStatus() {
		return status;
	}

	public void setStatus(HttpStatus status) {
		this.status = status;
	}

	public String getMessage() {
		return message;
	}

	public void setMessage(String message) {
		this.message = message;
	}

	public Object getErrors() {
		return errors;
	}

	public void setErrors(Object errors) {
		this.errors = errors;
	}

}

 

Step 5: Create a package ‘com.sample.app.exception.handler’ and define CustomErrorHandler class.

 

CustomErrorHandler.java

 

package com.sample.app.exception.handler;

import java.util.ArrayList;
import java.util.List;

import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

import com.sample.app.model.ApiError;

@ControllerAdvice(basePackages = "com.sample.app")
public class CustomErrorHandler extends ResponseEntityExceptionHandler {
	@Override
	protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex,
			HttpHeaders headers, HttpStatus status, WebRequest request) {

		List<String> errors = new ArrayList<String>();

		for (FieldError error : ex.getBindingResult().getFieldErrors()) {
			errors.add(error.getField() + " -> " + error.getDefaultMessage());
		}

		for (ObjectError error : ex.getBindingResult().getGlobalErrors()) {
			errors.add(error.getObjectName() + " -> " + error.getDefaultMessage());
		}

		ApiError apiError = new ApiError(HttpStatus.BAD_REQUEST, ex.getMessage(), errors);
		return handleExceptionInternal(ex, apiError, headers, apiError.getStatus(), request);

		// If you want to throw apiError directly, uncomment this
		// return new ResponseEntity(apiError, apiError.getStatus());
	}
}

 

Step 7: Create a package ‘com.sample.app.config’ and define SwaggerConfig class.

 

SwaggerConfig.java

package com.sample.app.config;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;

import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

@Component
@EnableAutoConfiguration
@EnableSwagger2
public class SwaggerConfig {

	@Bean
	public Docket userApi() {

		return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).select().paths(PathSelectors.any())
				.apis(RequestHandlerSelectors.basePackage("com.sample.app.controller")).build();
	}

	private ApiInfo apiInfo() {

		return new ApiInfoBuilder().title("Query builder").description("Query builder using spring specification")
				.version("2.0").build();
	}
}

 

Step 8: Create a package ‘com.sample.app.controller’ and define EmployeeController.

 

EmployeeController.java

 

package com.sample.app.controller;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

import javax.validation.Valid;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import com.sample.app.model.Employee;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;

@RestController
@RequestMapping("api/v1/employees")
@Api(tags = { "This section contains all Employee Speicifc Operations" })
public class EmployeeController {

	private static AtomicInteger counter = new AtomicInteger();

	private static Map<Integer, Employee> internalCache = new ConcurrentHashMap<>();

	@RequestMapping(method = RequestMethod.POST)
	@ApiOperation(value = "Create new employee", notes = "Create new employee")
	public ResponseEntity<Employee> create(@RequestBody @Valid Employee employee) {
		int newId = counter.incrementAndGet();
		employee.setId(newId);

		internalCache.put(newId, employee);

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

 

Step 9: Define App class in com.sample.app package.

 

App.java

 

package com.sample.app;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class App {

	public static void main(String args[]) {
		SpringApplication.run(App.class, args);
	}

}

Total project structure looks like below.


 

 

Run App.java.

 

Open the url ‘http://localhost:1234/swagger-ui.html’ and work with the api ‘POST /api/v1/employees’ with the below payload.

{

  "lastName": "PTR"

}

 

You will see following error message in swagger response body.

{
  "status": "BAD_REQUEST",
  "message": "Validation failed for argument [0] in public org.springframework.http.ResponseEntity<com.sample.app.model.Employee> com.sample.app.controller.EmployeeController.create(com.sample.app.model.Employee): [Field error in object 'employee' on field 'firstName': rejected value [null]; codes [NotNull.employee.firstName,NotNull.firstName,NotNull.java.lang.String,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [employee.firstName,firstName]; arguments []; default message [firstName]]; default message [Firstname can't be null]] ",
  "errors": [
    "firstName -> Firstname can't be null"
  ]
}

Now pass empty object {} as request payload, you will see following error message.

{
  "status": "BAD_REQUEST",
  "message": "Validation failed for argument [0] in public org.springframework.http.ResponseEntity<com.sample.app.model.Employee> com.sample.app.controller.EmployeeController.create(com.sample.app.model.Employee) with 2 errors: [Field error in object 'employee' on field 'firstName': rejected value [null]; codes [NotNull.employee.firstName,NotNull.firstName,NotNull.java.lang.String,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [employee.firstName,firstName]; arguments []; default message [firstName]]; default message [Firstname can't be null]] [Field error in object 'employee' on field 'lastName': rejected value [null]; codes [NotNull.employee.lastName,NotNull.lastName,NotNull.java.lang.String,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [employee.lastName,lastName]; arguments []; default message [lastName]]; default message [LastName can't be null]] ",
  "errors": [
    "firstName -> Firstname can't be null",
    "lastName -> LastName can't be null"
  ]
}

You can download complete working application from this link.


 

Previous                                                    Next                                                    Home

1 comment: