I am sending a DTO from client with a failed validation for one field. I tried to handle it in the Controller Advice, but it turns out it is not the instance of the ValidationException.class as that handler does not work. I checked it using the #ExceptionHandler(Exception.class) and it worked as expected, but I want to specify the Exception type properly.
This is the DTO:
public class BookDto {
#NotBlank(message = "Name of the book cannot be empty!")
#Size(min = 4, max = 30, message = "Name of the book must be between 4 and 30 characters
long!")
public String name;
#NotBlank(message = "Author of the book cannot be empty!")
#Size(min = 4, max = 35, message = "Author of the book must be between 5 and 35
characters long!")
public String author;
#JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd")
public LocalDate publicationYear;
#NotNull(message = "Pages of the book cannot be null!")
#Digits(integer = 4, fraction = 0)
public int pagesNumber;
#NotBlank(message = "Publisher of the book cannot be empty!")
#Size(min = 4, max = 30, message = "Publisher of the book must be between 4 and 30
characters long!")
public String publisher;
}
This is the ControllerAdvice:
#ControllerAdvice
public class GlobalExceptionHandler {
#ExceptionHandler(ResourceNotFoundException.class)
protected ResponseEntity<?> resourceNotFoundException(ResourceNotFoundException ex,
WebRequest request) {
ErrorDetails errorDetails = new ErrorDetails(new Date(), ex.getMessage(),
request.getDescription(false));
return new ResponseEntity<>(errorDetails, HttpStatus.NOT_FOUND);
}
#ExceptionHandler(ResourceWasDeletedException.class)
protected ResponseEntity<MyGlobalExceptionHandler> handleDeleteException() {
return new ResponseEntity<>(new MyGlobalExceptionHandler("This book was deleted!"),
HttpStatus.NOT_FOUND);
}
#ExceptionHandler(ValidationException.class)
public ResponseEntity<?> handleValidationException(ValidationException exception,
WebRequest webRequest) {
ErrorDetails errorDetails = new ErrorDetails(new Date(), exception.getMessage(),
webRequest.getDescription(false));
return new ResponseEntity<>(errorDetails, HttpStatus.BAD_REQUEST);
}
/* #ExceptionHandler(Exception.class)
public ResponseEntity<?> globalExceptionHandler(Exception ex, WebRequest request) {
ErrorDetails errorDetails = new ErrorDetails(new Date(), ex.getMessage(),
request.getDescription(false));
return new ResponseEntity<>(errorDetails, HttpStatus.INTERNAL_SERVER_ERROR);
}*/
#Data
#AllArgsConstructor
private static class MyGlobalExceptionHandler {
private String message;
}
}
This is the post method:
#PostMapping("/books")
#ResponseStatus(HttpStatus.CREATED)
public BookDto createBook(#Valid #RequestBody BookDto requestForSave){
var book = converter.getMapperFacade().map(requestForSave, Book.class);
log.debug("Book={}", book);
var bookDto = converter.toDto((Book) bookService.create(book));
log.debug("Book={}", bookDto);
return bookDto;
}
When I commented the last Exception handler Postman receives a response with this:
So the ValidationException handler did not work. What type of the Exception is this?
You can figure that out yourself: Restore your commented-out catch-all exception handler and just print or otherwise report ex.getClass(). exceptions are objects same as any other. They have a type, you can instanceof them, you can call .getClass() on them, you can pass them around like any other.
I recently implemented authentication in my API Rest, but when the user tries to access to api without being auntenticatedn the exception message doesn't appear:
This is my GlobalExceptionHandler with the method exception that cover all errors in the api:
#ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
#ExceptionHandler(Exception.class)
public ResponseEntity<ErrorDetails> handlerExceptionMethod(Exception ex , WebRequest
webRequest){
ErrorDetails errorDetails = new ErrorDetails(new Date(), ex.getMessage(),
webRequest.getDescription(false));
return new ResponseEntity<ErrorDetails>(errorDetails,
HttpStatus.INTERNAL_SERVER_ERROR);
}
The Postman sofware returns an 1, I want to see the errorDetails parameters. This is my ErrorDetails class:
#Getter
public class ErrorDetails {
private Date timestamp;
private String message;
private String details;
public ErrorDetails(Date timestamp, String message, String details) {
this.timestamp = timestamp;
this.message = message;
this.details = details;
}
}
I solved the problem by addding the following line of code in Security COnfig class:
.antMatchers("/error").permitAll()
I'm trying to create a #ControllerAdvice class which currently handle a MethodArgumentNotValidException exception. I've created an exception wrapper for the response which have an errorMessage and statusCode attribute.
public class ExceptionBodyResponse {
private String exceptionMessage;
private int statusCode;
}
And the #ControllerAdvice :
#ControllerAdvice
public class DTOExceptionHandler {
#ExceptionHandler(MethodArgumentNotValidException.class)
#ResponseBody
public ExceptionBodyResponse handleInvalidArgumentException(MethodArgumentNotValidException exception) {
return GenericBuilder.of(ExceptionBodyResponse::new)
.with(ExceptionBodyResponse::setExceptionMessage, exception.getMessage())
.with(ExceptionBodyResponse::setStatusCode, HttpStatus.BAD_REQUEST.value())
.build();
}
}
And finally, the DTO class with #NotBlank validation:
public class RegisterRequestDto {
#NotBlank
private String email;
#NotBlank(message = "Password must not be null!")
private String password;
}
What I'm expecting as response when I'm sending a JSON with this structure :
{
"email":"stack#yahoo.com";
}
is the following error message:
{
"exceptionMessage":"Password must not be null",
"statusCode":400
}
Instead, I'm getting this:
"exceptionMessage": "Validation failed for argument [0] in public packagehere.UserDto packagehere.UserControllerImpl.save(packagehere.RegisterRequestDto): [Field error in object 'registerRequestDto' on field 'password': rejected value [null]; codes [NotBlank.registerRequestDto.password,NotBlank.password,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [registerRequestDto.password,password]; arguments []; default message [password]]; default message [Password must not be null!]] ",
"statusCode": 400
Try this.
#ControllerAdvice
#Order(Ordered.HIGHEST_PRECEDENCE)
public class DTOExceptionHandler extends ResponseEntityExceptionHandler{
#Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(
final MethodArgumentNotValidException exception,
final HttpHeaders headers,
final HttpStatus status,
final WebRequest request) {
return GenericBuilder.of(ExceptionBodyResponse::new)
.with(ExceptionBodyResponse::setExceptionMessage, exception.getMessage())//You can customize message
.with(ExceptionBodyResponse::setStatusCode, HttpStatus.BAD_REQUEST.value())
.build();
}
}
You can make use of ex.getBindingResult().getFieldErrors() to get the list of FieldError
You need to override method inside #ControllerAdvice :
#Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
List<String> details = new ArrayList<>();
for (FieldError fieldError : ex.getBindingResult().getFieldErrors()) {
details.add("{\"" + fieldError.getField() + "\" : \"" + fieldError.getDefaultMessage() + "\"}");
}
return new ResponseEntity<>(details.toString(), BAD_REQUEST);
}
Response is JSON and looks like :
[
{
"employeeLogin": "Pole wymagane"
},
{
"type": "Pole wymagane"
},
{
"description": "Pole wymagane"
},
{
"name": "Pole wymagane"
}]
where validated object is :
#NotBlank(message = "Pole wymagane")
private String employeeLogin;
#NotBlank(message = "Pole wymagane")
private String type;
#NotBlank(message = "Pole wymagane")
private String name;
#NotBlank(message = "Pole wymagane")
private String description;
Described step by step and solved here :
https://howtodoinjava.com/spring-boot2/spring-rest-request-validation/
I'm wondering about proper way of implementating of controller in case of GET request with multiple request params given. In my understanding of REST it's much better to have one endpoint with additional parameters for filtering/sorting than several endpoints (one for each case). I'm just wondering about maintanance and extensibility of such endpoint. Please have a look on example below :
#RestController
#RequestMapping("/customers")
public class CustomerController {
#Autowired
private CustomerRepository customerRepo;
#GetMapping
public Page<Customer> findCustomersByFirstName(
#RequestParam("firstName") String firstName,
#RequestParam("lastName") String lastName,
#RequestParam("status") Status status, Pageable pageable) {
if (firstName != null) {
if (lastName != null) {
if (status != null) {
return customerRepo.findByFirstNameAndLastNameAndStatus(
firstName, lastName, status, pageable);
} else {
return customerRepo.findByFirstNameAndLastName(
firstName, lastName, pageable);
}
} else {
// other combinations omitted for sanity
}
} else {
// other combinations omitted for sanity
}
}
}
Such endpoint seems to be very convenient to use (order of parameters doesn't matter, all of them are optional...), but maintaining something like this looks like a hell (number of combinations can be enormous).
My question is - what is the best way to deal with something like this? How is it designed in "professional" APIs?
What is the best way to deal with something like this?
The best way to deal with it is to use the tools already available. As you are using Spring Boot and, I assume therefore, Spring Data JPA then enable the QueryDsl suppport and web support extensions for Spring Data JPA.
You controller then simply becomes:
#GetMapping
public Page<Customer> searchCustomers(
#QuerydslPredicate(root = Customer.class) Predicate predicate, Pageable pageable) {
return customerRepo.findBy(predicate, pageable);
}
and your repository is simply extended to support QueryDsl:
public interface CustomerRepository extends PagingAndSortingRepository<Customer, Long>,
QueryDslPredicateExecutor<Customer>{
}
You can now query by any combination of params without writing any further code.
https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#core.web.type-safe
https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#core.extensions.querydsl
Good day. I can't call myself a professional, but here are some tips which can make this controller looks better.
Use DTO instead of using a group of parameters
public class CustomerDTO {
private String firstName;
private String lastName;
private String status;
}
With this class your method's signature will look like this:
#GetMapping
public Page<Customer> findCustomersByFirstName(CustomerDTO customerDTO, Pageable pageable) {
...
}
Use validation if you need one
For example, you can make some of these fields are required:
public class CustomerDTO {
#NotNull(message = "First name is required")
private String firstName;
private String lastName;
private String status;
}
Don't forget to add #Valid annotation before the DTO parameter in your controller.
Use specification instead of this block with if-else
Here is a great guide on how to do it - REST Query Language with Spring Data JPA Specifications
Use the service layer, don't need to call repository from the controller
#GetMapping
public Page<Customer> findCustomersByFirstName(#Valid CustomerDTO customerDTO, BindingResult bindingResult, Pageable pageable) {
if (bindingResult.hasErrors()) {
// error handling
}
return customerService.findAllBySpecification(new CustomerSpecification(customerDTO));
}
Your controller should not contain any logic about working with entities or some business stuff. It's only about handling request/errors, redirects, views, etc...
Its good to have a POST request with such validations instead of a GET request.You can use following method for the controller.
#PostMapping(value = "/findCustomer",produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<?> findCustomersByFirstName(#Valid #RequestBody Customer customer){
return customerRepo.findByFirstNameAndLastNameAndStatus(customer.getFirstName, customer.getLastName(), customer.getStatus(), pageable);
}
use the DTO as follows.
public class Customer {
private String firstName;
private String lastName;
private String status;
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;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status= status;
}
public LivenessInputModel(String firstName, String lastName, String status) {
this.firstName= firstName;
this.lastName= lastName;
this.status= status;
}
public LivenessInputModel() {
}
}
And add a controller level exception advice to return the response in errors.
#ControllerAdvice
public class ControllerExceptionAdvice {
private static final String EXCEPTION_TRACE = "Exception Trace:";
private static Logger log = LoggerFactory.getLogger(ControllerExceptionAdvice.class);
public ControllerExceptionAdvice() {
super();
}
#ExceptionHandler({ BaseException.class })
public ResponseEntity<String> handleResourceException(BaseException e, HttpServletRequest request,
HttpServletResponse response) {
log.error(EXCEPTION_TRACE, e);
HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.setContentType(MediaType.APPLICATION_JSON);
BaseExceptionResponse exceptionDto = new BaseExceptionResponse(e);
return new ResponseEntity<>(exceptionDto.toString(), responseHeaders, e.getHttpStatus());
}
#ExceptionHandler({ Exception.class })
public ResponseEntity<String> handleException(Exception e, HttpServletRequest request,
HttpServletResponse response) {
log.error(EXCEPTION_TRACE, e);
HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.setContentType(MediaType.APPLICATION_JSON);
HttpStatus httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;
BaseExceptionResponse exceptionDto = new BaseExceptionResponse(httpStatus.value(),
ExceptionMessages.INTERNAL_DEFAULT);
return new ResponseEntity<>(exceptionDto.toString(), responseHeaders, httpStatus);
}
#ExceptionHandler({ MethodArgumentNotValidException.class })
public ResponseEntity<String> handleValidationException(MethodArgumentNotValidException e,
HttpServletRequest request, HttpServletResponse response) {
log.error(EXCEPTION_TRACE, e);
HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.setContentType(MediaType.APPLICATION_JSON);
ValidationException validationEx = new ValidationException(e);
BaseExceptionResponse exceptionDto = new BaseExceptionResponse(validationEx);
return new ResponseEntity<>(exceptionDto.toString(), responseHeaders, validationEx.getHttpStatus());
}
#ExceptionHandler({ HttpMediaTypeNotSupportedException.class, InvalidMimeTypeException.class,
InvalidMediaTypeException.class, HttpMessageNotReadableException.class })
public ResponseEntity<String> handleMediaTypeNotSupportException(Exception e, HttpServletRequest request,
HttpServletResponse response) {
log.error(EXCEPTION_TRACE, e);
HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.setContentType(MediaType.APPLICATION_JSON);
HttpStatus httpStatus = HttpStatus.BAD_REQUEST;
BaseExceptionResponse exceptionDto = new BaseExceptionResponse(httpStatus.value(),
ExceptionMessages.BAD_REQUEST_DEFAULT);
return new ResponseEntity<>(exceptionDto.toString(), responseHeaders, httpStatus);
}
#ExceptionHandler({ HttpRequestMethodNotSupportedException.class })
public ResponseEntity<String> handleMethodNotSupportException(Exception e, HttpServletRequest request,
HttpServletResponse response) {
log.error(EXCEPTION_TRACE, e);
HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.setContentType(MediaType.APPLICATION_JSON);
HttpStatus httpStatus = HttpStatus.METHOD_NOT_ALLOWED;
BaseExceptionResponse exceptionDto = new BaseExceptionResponse(httpStatus.value(),
ExceptionMessages.METHOD_NOT_ALLOWED);
return new ResponseEntity<>(exceptionDto.toString(), responseHeaders, httpStatus);
}
#ExceptionHandler({ MissingServletRequestParameterException.class })
public ResponseEntity<String> handleMissingServletRequestParameterException(Exception e, HttpServletRequest request,
HttpServletResponse response) {
log.error(EXCEPTION_TRACE, e);
HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.setContentType(MediaType.APPLICATION_JSON);
HttpStatus httpStatus = HttpStatus.BAD_REQUEST;
BaseExceptionResponse exceptionDto = new BaseExceptionResponse(httpStatus.value(),
ExceptionMessages.BAD_REQUEST_DEFAULT);
return new ResponseEntity<>(exceptionDto.toString(), responseHeaders, httpStatus);
}
}
Actually, you answered half of the answer yourself, Query Params are used for filtration purposes, and as you can see in your code this will be allowed via GET request. But your question regarding validations is something a trade-off.
For example; if you don't want to have this kind of check, you can depend on mandatory required = true which is the default in #RequestParam, and handle it in the response immediately.
Or you can use alternatively #RequestBody with support of #Valid for more clear info for what wrong had occurred; for example
#PostMapping(value = "/order")
public ResponseEntity<?> submitRequest(#RequestBody #Valid OrderRequest requestBody,
Errors errors) throws Exception {
if (errors.hasErrors())
throw new BusinessException("ERR-0000", "", HttpStatus.NOT_ACCEPTABLE);
return new ResponseEntity<>(sendOrder(requestBody), HttpStatus.CREATED);
}
// Your Pojo
public class OrderRequest {
#NotNull(message = "channel is required")
private String channel;
#NotNull(message = "Party ID is required")
private long partyId;
}
For more information check this #Valid usage in Spring
This way will decouple your validation mechanism from Controller layer to Business layer. which in turns will save lots of boilerplate code, but as you noticed with change to POST instead.
So in general, there is no direct answer to your question, and the short answer is it depends, so choose whatever easy for you with good capabilities and less maintainance will be the best practice
As an alternative solution besides other ones, you can use JpaSpecificationExecutor<T> in your repository and create a specification object based on your arguments and pass it to the findAll method.
So, your repository should extend from the JpaSpecificationExecutor<Customer> interface as follows:
#Repository
interface CustomerRepository extends JpaSpecificationExecutor<Customer> {
}
Your controller should get the required arguments as Map<String, String to gain dynamic behavior.
#RestController
#RequestMapping("/customers")
public class CustomerController {
private final CustomerRepository repository;
#Autowired
public CustomerController(CustomerRepository repository) {
this.repository = repository;
}
#GetMapping
public Page<Customer> findAll(#RequestBody HashMap<String, String> filters, Pageable pageable) {
return repository.findAll(QueryUtils.toSpecification(filters), pageable);
}
}
And, you should define a method to convert provided arguments to Specification<Customer>:
class QueryUtils {
public static Specification<Customer> toSpecification(Map<String, String> filters) {
Specification<Customer> conditions = null;
for (Map.Entry<String, String> entry : filters.entrySet()) {
Specification<Customer> condition = Specification.where((root, query, cb) -> cb.equal(root.get(entry.getKey()), entry.getValue()));
if (conditions == null) {
conditions = condition;
} else {
conditions = conditions.and(condition);
}
}
return conditions;
}
}
Also, You can use the Meta model to make better criteria query and combine it with the provided solution.
I have the following example:
This is the request body:
public class UserLoginData
implements Serializable {
private static final long serialVersionUID = 1L;
private String username;
private String password;
//... getter and setters
}
This is the Controller:
#RequestMapping(value = {"/login"}, method = RequestMethod.POST)
#ResponseBody
public LoginResponse login(#RequestBody(required = true) UserLoginData loginData){
//... some code
}
This is how I invoke the service:
POST /login
{"username":"neuquino"}
I expect that Spring returns a HTTP 400 BAD REQUEST error, because password is missing. But instead of that, it returns a HTTP 500 INTERNAL SERVER error with the following stacktrace:
java.lang.NullPointerException
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:948) ~[spring-webmvc-3.2.2.RELEASE.jar:3.2.2.RELEASE]
at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:838) ~[spring-webmvc-3.2.2.RELEASE.jar:3.2.2.RELEASE]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:755)
...
How can I specify to Spring that username and password are required fields in request body?
#Bart's answer was very useful to find my final solution:
public class UserLoginData
implements Serializable {
private static final long serialVersionUID = 1L;
#NotNull
#NotBlank
private String username;
#NotNull
#NotBlank
private String password;
//... getter and setters
}
On my Controller I have:
public LoginResponse login(
#RequestBody(required = true) #Valid UserLoginData loginData){
//... login code
}
Until here is very similar, but it is clearer because the controller's method does not have the error validation. Instead of that, I used another class with the ControllerAdvice annotation
#ControllerAdvice
public class RestErrorHandler {
private MessageSource messageSource;
#Autowired
public RestErrorHandler(#Qualifier("messageSource") MessageSource messageSource) {
this.messageSource = messageSource;
}
#ExceptionHandler(MethodArgumentNotValidException.class)
#ResponseStatus(HttpStatus.BAD_REQUEST)
#ResponseBody
public ValidationError processValidationError(MethodArgumentNotValidException ex) {
BindingResult result = ex.getBindingResult();
List<FieldError> fieldErrors = result.getFieldErrors();
return this.processFieldErrors(fieldErrors);
}
private ValidationError processFieldErrors(List<FieldError> fieldErrors) {
ValidationError dto = new ValidationError();
for (FieldError fieldError : fieldErrors) {
String localizedErrorMessage = this.resolveLocalizedErrorMessage(fieldError);
dto.getErrors().put(fieldError.getField(), localizedErrorMessage);
}
return dto;
}
private String resolveLocalizedErrorMessage(FieldError fieldError) {
Locale currentLocale = LocaleContextHolder.getLocale();
String localizedErrorMessage = this.messageSource.getMessage(fieldError, currentLocale);
return localizedErrorMessage;
}
}
Now my service is returning this:
{
"errors":{
"country":"country cannot be null"
}
}
I hope it helps someone else.
To get this solution I also used what is written in this post.
If the password is missing it will not be set when the UserLoginData object is created. It will not check if the value is valid or anything. If you need to validate your login data use proper validation.
You could use the annotations in the hibernate validator package for declarative validation e.g.
public class UserLoginData
implements Serializable {
private static final long serialVersionUID = 1L;
#NotNull
#NotBlank
private String username;
#NotNull
#NotBlank
private String password;
//... getter and setters
}
Your method could then be written as (note the #Valid annotation):
public LoginResponse login(
#RequestBody(required = true) #Valid UserLoginData loginData,
BindingResult result,
HttpServletResponse response){
if (result.hasErrors()) {
// Validation problems!
response.sendError(400, "Bad login data");
}
}