When using Spring Data REST and JSR 303 Bean Validation I get a response like the following when there's a constraint violation:
{
"errors": [
{
"entity": "Empresa",
"property": "observacao",
"invalidValue": "",
"message": "O tamanho do campo deve ser entre 1 e 255"
}
]
}
But I'm trying to validate an object manually, and I would like to return the validation errors in the same format used by Spring Data Rest.
#DeleteMapping("/departamento/{id}")
public #ResponseBody
ResponseEntity<?> filtro(#PathVariable Long id){
Optional<Departamento> departamentoOpt = this.departamentoRepository.findById(id);
if (!departamentoOpt.isPresent()) {
return ResponseEntity.notFound().build();
}
Departamento departamento = departamentoOpt.get();
BindingResult errors = new BeanPropertyBindingResult(
departamento, "departamento");
this.validator.validate(departamento, errors, PreDeleteValidation.class);
if (errors.hasErrors()) {
// How to return a response in the same format used by SDR here?
}
return ResponseEntity.ok().build();
}
How can this be accomplished?
You can throw and Exception on validation failure and register a Spring MVC Controller Advice to catch this and transform it to something that meets your needs.
if (errors.hasErrors()) {
throw new org.springframework.web.bind.MethodArgumentNotValidException(
departamento, bindingResult)
}
The advice could look something like the below:
#ControllerAdvice
public class ErrorHandlingAdvice
{
#ExceptionHandler(MethodArgumentNotValidException.class)
#ResponseStatus(HttpStatus.BAD_REQUEST)
#ResponseBody
public ValidationError processValidationError(MethodArgumentNotValidException ex)
{
ValidationError error = new ValidationError();
BindingResult result = ex.getBindingResult();
List<FieldError> fieldErrors = result.getFieldErrors();
for (FieldError fieldError : fieldErrors)
{
error.addError(fieldError.getField(), fieldError.getDefaultMessage());
}
return error;
}
}
ValidationError is just a simple bean:
public class ValidationError
{
private final Map<String, List<String>> errors;
public ValidationError()
{
errors = new TreeMap<>();
}
public void addError(String field, String error)
{
if (!errors.containsKey(field))
{
errors.put(field, new ArrayList<String>());
}
errors.get(field).add(error);
}
public Map<String, List<String>> getErrors()
{
return errors;
}
}
Related
For example:
#GetMapping("/exception/{id}")
public ResponseEntity getException(#PathVariable #Min(1) Integer id) {
return ResponseEntity.ok().body(id.toString());
}
it must returns "must be greater than or equal to 1" (default message) when I call this function with id=-5 from the eng page. And something like this "doit être supérieur ou égal à 1", when i coll it from french page.
hibernate-validator jar file contains different localization messages. You need to configure based on Accept-Language header in the request it will decide which message to be displayed.
#Bean
public AcceptHeaderLocaleResolver localeResolver() {
final AcceptHeaderLocaleResolver resolver = new AcceptHeaderLocaleResolver();
resolver.setDefaultLocale(Locale.US);
return resolver;
}
The Response DTO object.
#Data
public class ErrorDto {
private LocalDateTime timestamp;
private HttpStatus status;
private String error;
public ErrorDto(HttpStatus status, String error) {
timestamp = LocalDateTime.now();
this.status = status;
this.error = error;
}
}
Then in ControllerAdvice catch ConstraintViolationException and show a response.
#ResponseStatus(code = HttpStatus.BAD_REQUEST)
#ExceptionHandler({ConstraintViolationException.class})
public ResponseEntity<Object> handleConstraintViolation(ConstraintViolationException ex, WebRequest request) {
String error = "";
for (ConstraintViolation<?> violation : ex.getConstraintViolations()) {
error = violation.getMessage();
}
ErrorDto errorDto = new ErrorDto(HttpStatus.BAD_REQUEST, error);
return new ResponseEntity<>(errorDto, errorDto.getStatus());
}
It will give you response for given controller.
{
"timestamp": "2022-10-23T21:08:07.0459025",
"status": "BAD_REQUEST",
"error": "doit être supérieur ou égal à 1"
}
I want to validate a requestbody with #Valid annotation.
In the method there is a #RequestParam value.
If this #RequestParam value "min" I want that a field, in requestbody, is mandatory. It's possible with annotations?
#RequestMapping(method = RequestMethod.POST, value = ProductionResponse.URL, produces = JWSMessageConverter.JWS_MEDIA_TYPE_VALUE)
#ResponseBody
public ResponseEntity<ProductionResponse> richiestaProduzione(#RequestParam("issuerType") String issuerType,
#RequestParam("issuerCode") String issuerCode, #RequestParam("procedureId") String procedureId,
#Valid #RequestBody ProductionRequestResource requestBody) {
if (LOGGER.isDebugEnabled())
LOGGER.debug("POST " + ProductionResponse.URL);
#Valid ProductionRequestResource fff = requestBody;
requestBody.setProcedureResource(issuerType, issuerCode, procedureId);
try {
ProductionResponse response = produzioneService.richiestaProduzione(requestBody);
return ResponseEntity.ok(response);
} catch (Exception e) {
LOGGER.error(e.getMessage(), e);
return ResponseEntity.badRequest().body(ProductionResponse.ko(requestBody, e.getMessage(), requestBody.getNun()));
}
}
I want that the field issuerMunicipalityCode in the ProductionRequestResource is mandatory if issuerType is equal to "MIN".
To continue further, here is how you can implement custom validation.
Add a validation to throw an exception when issueType is MIN and municipality code is empty/null.
#RequestMapping(method = RequestMethod.POST, value = ProductionResponse.URL, produces = JWSMessageConverter.JWS_MEDIA_TYPE_VALUE)
#ResponseBody
public ResponseEntity<ProductionResponse> richiestaProduzione(#RequestParam("issuerType") String issuerType,
#RequestParam("issuerCode") String issuerCode, #RequestParam("procedureId") String procedureId,
#RequestBody ProductionRequestResource requestBody) {
if (LOGGER.isDebugEnabled())
LOGGER.debug("POST " + ProductionResponse.URL);
if("MIN".equals(issuerType) && StringUtils.isEmpty(requestBody.getIssuerMunicipalityCode())) {
throw new IllegalArgumentException("IssuerMuncipalityCode can't be null when IssuerType is MIN");
}
try {
ProductionResponse response = produzioneService.richiestaProduzione(requestBody);
return ResponseEntity.ok(response);
} catch (Exception e) {
LOGGER.error(e.getMessage(), e);
return ResponseEntity.badRequest().body(ProductionResponse.ko(requestBody, e.getMessage(), requestBody.getNun()));
}
}
Implement exception handler to catch the exceptions and build response body based on exception types. InvalidArgumentSuppliedException that was thrown above will be caught by below exception handler. Exception Handler is a best practice to handle exceptions in a separate component.
#ControllerAdvice
public class ScnSchedulerExceptionHandler {
#ExceptionHandler(InvalidArgumentSuppliedException.class)
public final ResponseEntity<Object> handleInvalidArgException(Exception ex) {
ErrorResponseDTO errorDTO = new ErrorResponseDTO();
errorDTO.setMessage(ex.getMessage());
return new ResponseEntity<>(errorDTO, HttpStatus.NOT_ACCEPTABLE);
}
#ExceptionHandler(Exception.class)
public final ResponseEntity<Object> handleAllExceptions(Exception ex) {
ErrorResponseDTO errorDTO = new ErrorResponseDTO();
errorDTO.setMessage("Internal server error occurred.");
return new ResponseEntity<>(errorDTO, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
In the exception handler, wrap the error messages in a pojo object.
public class ErrorResponseDTO {
private String errorMessage;
}
I'm coding REST Api-s in spring boot. I want to make sure that my code is readable to front-end developers using swagger API development tool (Swagger). For example
#GetMapping("/getOne")
public ResponseEntity<?> getOne(#RequestParam String id) {
try {
return new ResponseEntity<Branch>(branchService.getOne(id), HttpStatus.OK);
} catch (Exception e) {
return new ResponseEntity<FindError>(new FindError(e.getMessage()), HttpStatus.BAD_REQUEST);
}
}
If the request is successful, response is a Branch object, if fails, the response is a FindError object which has only one attribute (message). So both can be carried out depends on the response. But the swagger UI doesn't show how the response should be shown, because I use "?" as generic type. Is this a best practice to catch an error? (This coding documentation swagger is not useful to front-end developers since it doesn't show the response object). Or any best practice for the above problem?
There are a lot of method which return different object like Branch. Thanks in advance
First of all you should follow the best practices of a RESTful API . Don't use verbs, instead use nouns as URL.So instead of #GetMapping("/getOne") , you can write
it as #GetMapping("/branch/{id}") .
You can refer this blog https://blog.mwaysolutions.com/2014/06/05/10-best-practices-for-better-restful-api/
#2ndly , Don't return a generic type as ? , instead you can user the specific type , here as Branch and do central exception handling .
The following code snippet can help you :
#GetMapping("/branch/{id}")
public ResponseEntity<Branch> getBranch(#Pathvariable String id) {
{
Branch branch = branchService.getOne(id);
if(branch == null) {
throw new RecordNotFoundException("Invalid Branch id : " + id);
}
return new ResponseEntity<Branch>(branch, HttpStatus.OK);
}
RecordNotFoundException.java
#ResponseStatus(HttpStatus.NOT_FOUND)
public class RecordNotFoundException extends RuntimeException
{
public RecordNotFoundException(String exception) {
super(exception);
}
}
CustomExceptionHandler.java
#ControllerAdvice
public class CustomExceptionHandler extends ResponseEntityExceptionHandler
{
#ExceptionHandler(Exception.class)
public final ResponseEntity<Object> handleAllExceptions(Exception ex, WebRequest request) {
List<String> details = new ArrayList<>();
details.add(ex.getLocalizedMessage());
ErrorResponse error = new ErrorResponse("Server Error", details);
return new ResponseEntity(error, HttpStatus.INTERNAL_SERVER_ERROR);
}
#ExceptionHandler(RecordNotFoundException.class)
public final ResponseEntity<Object> handleRecordNotFoundException(RecordNotFoundException ex, WebRequest request) {
List<String> details = new ArrayList<>();
details.add(ex.getLocalizedMessage());
ErrorResponse error = new ErrorResponse("Record Not Found", details);
return new ResponseEntity(error, HttpStatus.NOT_FOUND);
}
}
ErrorResponse.java
public class ErrorResponse
{
public ErrorResponse(String message, List<String> details) {
super();
this.message = message;
this.details = details;
}
private String message;
private List<String> details;
//Getter and setters
}
The above class handles multiple exceptions including RecordNotFoundException and you can also customize for payload validations too.
Test Cases :
1) HTTP GET /branch/1 [VALID]
HTTP Status : 200
{
"id": 1,
"name": "Branch 1",
...
}
2) HTTP GET /branch/23 [INVALID]
HTTP Status : 404
{
"message": "Record Not Found",
"details": [
"Invalid Branch id : 23"
]
}
I would recommend to do it like this .
#GetMapping("/getOne")
public Response getOne(#RequestParam String id) {
ResponseEntity<Branch> resbranch;
ResponseEntity<FindError> reserror;
try {
resbranch=new ResponseEntity<Branch>(branchService.getOne(id), HttpStatus.OK);
return Response.status(200).entity(resbranch).build();
} catch (Exception e) {
reserror=new ResponseEntity<FindError>(new FindError(e.getMessage()), HttpStatus.BAD_REQUEST);
return Response.status(400).entity(reserror).build();
}
}
200 is for OK and 400 is for bad request. Here there wont be anymore ambiguous types..
When building a rest api using spring boot what is the best way to handle exceptions from the service level and pass them to the controller, so the client gets a custom json error message.
{
"message": "some error"
}
Endpoint from controller
#PostMapping("/login")
public String login(#RequestBody #Valid LoginDto loginDto) {
return gson.toJson(userService.login(loginDto.getUsername(), loginDto.getPassword()));
}
Service level code
public LoginResponseDto login(String username, String password) {
try {
//performs some checks
...
return new LoginResponseDto(token.get());
} catch (AuthenticationException e){
LOGGER.info("Log in failed for user {}", username);
}
return new LoginResponseDto("login failed");
}
LoginResponseDto class
String token;
String message;
public LoginResponseDto(String message) {
this.message = message;
}
Currently it is obviously returning the correctly message but not the correct status code, it will show status 200 with the error message in json.
You have some options:
1) Returning a message:
If you want to return a message something like this,
{
"message": "some error"
}
What you can do is:
Option 1: Create a custom POJO class for error message and return the reference to the object of that POJO class.
Something like this:
ErrorMessage.java
package org.example;
public class ErrorMessage {
private String message;
public ErrorMessage(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
Request Handler Method in Controller:
#GetMapping("/login{?username, password}")
public ErrorMessage isUserAuthenticated(#RequestParam String username, #RequestParam String password) {
if (username.toLowerCase().contentEquals("root") && password.contentEquals("system")) {
return new ErrorMessage("authenticated");
}
return null;
}
Option 2: Create a Map and insert key-value pairs that you want to have in the message.
Like this:
#GetMapping("/login{?username, password}")
public Map<String, String> isUserAuthenticated(#RequestParam String username, #RequestParam String password) {
Map<String, String> message = new HashMap<>();
if (username.toLowerCase().contentEquals("root") && password.contentEquals("system")) {
message.put("message", "authenticated");
}
return message;
}
2) Returning an error status code (highly recommended by me):
You may use ResponseEntity for this purpose.
#GetMapping("/login{?username, password}")
public ResponseEntity<?> isUserAuthenticated(#RequestParam String username, #RequestParam String password) {
if (username.toLowerCase().contentEquals("root") && password.contentEquals("system")) {
return new ResponseEntity<>(HttpStatus.OK);
}
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
I am using Spring MVC and I am making a Validator but it looks like Spring is never running it.
Here is my Validator is a easy one right now just checking for two fields
public class MemberRequestValidator implements Validator {
public boolean supports(Class aClass) {
return MemberRequest.class.equals(aClass);
}
public void validate(Object obj, Errors errors) {
MemberRequest mr = (MemberRequest) obj;
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "content", "Content field is Required");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "areacode", "Area code field is Required");
}
}
Now my controller looks like the following:
#InitBinder("memberrequest")
public void initMemberRequestBinder(WebDataBinder binder) {
binder.setValidator(new MemberRequestValidator());
}
#RequestMapping(value = "/save", method = RequestMethod.POST)
public ModelAndView saveRequest(#ModelAttribute #Valid MemberRequest mr, BindingResult result)
{
if (result.hasErrors())
{
LOGGER.debug("Pages had errors on it... returning to input page");
return new ModelAndView("question");
}
else
{
String Ticket = mService.sentWebRequest(mr);
Map<String, Object> model = new HashMap<String, Object>();
Ticket t = new Ticket();
t.setTicketDetails(Ticket);
model.put("ticket", t);
return new ModelAndView("thanks", model);
}
}
and in my JSP page I have the following:
<c:url var="saveUrl" value="/mrequest/save.html" />
<form:form modelAttribute="memberrequest" action="${saveUrl}" name="memberrequest" id="memberrequest">
so if I dont enter any data in on the form I should hit the errors but I dont?
Try with #ModelAttribute("memberrequest") in handler or modelAttribute="memberRequest" in form and #initBinder("memberRequest")