I have a Spring Boot microservice and I want to valide the incoming requestBody of an endpoint.
By using #Valid with #NotBlank I have noticed that the answer is very verbose and my customized error message is deep into the object; here is an example:
{
"timestamp": "2020-12-17T09:28:26.529+0000",
"status": 400,
"error": "Bad Request",
"errors": [
{
"codes": [
"NotBlank.createUserRequest.username",
"NotBlank.username",
"NotBlank.java.lang.String",
"NotBlank"
],
"arguments": [
{
"codes": [
"createUserRequest.username",
"username"
],
"arguments": null,
"defaultMessage": "username",
"code": "username"
}
],
"defaultMessage": "USERNAME IS REQUIRED",
"objectName": "createUserRequest",
"field": "username",
"rejectedValue": "",
"bindingFailure": false,
"code": "NotBlank"
}
],
"message": "Validation failed for object='createUserRequest'. Error count: 1",
"path": "/api/user/create"
}
How can I customize this object returned? I would like the response to simply be something like this:
{
"timestamp": "2020-12-17T09:28:26.529+0000",
"status": 400,
"error": "Bad Request",
"message": "USERNAME IS REQUIRED"
}
Here is my code:
Request
#Data
public class CreateUserRequest {
#NotBlank(message = "username is required")
private String username;
#Size(min = 3, max = 64)
#NotBlank(message = "password is required")
private String password;
#NotBlank(message = "confirmPassword is required")
#Size(min = 3, max = 64)
private String confirmPassword;
}
Controller
#PostMapping("/create")
public ResponseEntity<User> createUser(#Valid #RequestBody CreateUserRequest request) {
User user = appService.createUserAndCart(request);
return ResponseEntity.ok(user);
}
Thank you for your experience
You can use #ControllerAdvice/#RestControllerAdvice
it allows you to handle exceptions across the whole application. You can think of it as an interceptor of exceptions thrown by methods annotated with #RequestMapping and similar.
And add a method like this,
#ExceptionHandler(Exception.class)
public ResponseEntity<Object> handleException(Exception ex)
//your custom body
return new ResponseEntity<>(body, HttpStatus.XXXXX);
}
You can specify a specific Exception type (I think it's InvalidArgumentException in your case)
Define a return class
import org.springframework.http.HttpStatus;
import java.util.HashMap;
/**
* #description:
* #author: 582895699#qq.com
* #time: 2020/12/20 下午 01:50
*/
public class Resp extends HashMap {
private static final long serialVersionUID = 1L;
public static final String TIMESTAMP = "timestamp";
public static final String STATUS = "status";
public static final String ERROR = "error";
public static final String MESSAGE = "message";
public static Resp fail(String message) {
Resp resp = new Resp();
resp.put(TIMESTAMP, System.currentTimeMillis());
resp.put(STATUS, HttpStatus.BAD_REQUEST.value());
resp.put(ERROR, HttpStatus.BAD_REQUEST.getReasonPhrase());
resp.put(MESSAGE, message);
return resp;
}
#Override
public Object put(Object key, Object value) {
return super.put(key, value);
}
}
Define global exception handling class and obtain exception information
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* #description:
* #author: 582895699#qq.com
* #time: 2020/12/20 下午 01:55
*/
#RestControllerAdvice
public class GlobalExceptionHandler {
#ExceptionHandler(value = MethodArgumentNotValidException.class)
public Resp methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
BindingResult bindingResult = e.getBindingResult();
ObjectError objectError = bindingResult.getAllErrors().get(0);
String message = objectError.getDefaultMessage();
return Resp.fail(message);
}
}
Related
I made very simple controller like below.
#PostMapping("/books")
public void create(#Valid #RequestBody BookPayload bookPayload) {
}
#Getter
#Setter
public class BookPayload {
#NotBlank
private String name;
#NotBlank
private String author;
}
When I call this api without name. It responses like below.
{
"timestamp": "2022-03-26T14:06:43.564+00:00",
"path": "/books",
"status": 400,
"error": "Bad Request",
"requestId": "654248ee-5",
"errors": [
{
"codes": [
"NotBlank.bookPayload.name",
"NotBlank.name",
"NotBlank.java.lang.String",
"NotBlank"
],
"arguments": [
{
"codes": [
"bookPayload.name",
"name"
],
"arguments": null,
"defaultMessage": "name",
"code": "name"
}
],
... omit ...
}
]
}
You can see errors attribute in the response body.
But If I test this api with #SpringBootTest or #WebfluxTest, There is no errors attribute.
#Slf4j
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class CommonErrorResponseTest {
private final WebClient web;
public CommonErrorResponseTest(#LocalServerPort Integer port) {
web = WebClient.create("http://localhost:" + port);
}
#Test
void _400_badRequest_violation() {
BookPayload bookPayload = new BookPayload();
bookPayload.setAuthor("John");
Mono<String> stringMono = web.post().uri("/books")
.header("Content-Type", MediaType.APPLICATION_JSON_VALUE)
.bodyValue(bookPayload)
.exchangeToMono(response -> response.bodyToMono(String.class));
String body = stringMono.block();
log.info("body: {}", body);
}
}
console
body: {"timestamp":"2022-03-26T14:19:21.981+00:00","path":"/books","status":400,"error":"Bad Request","requestId":"68df2a79-1"}
I'd like to know why I'm getting different results.
Spring Boot’s DevTools enables the inclusion of binding errors in the error response to ease problem solving during development. You can configure the same behaviour in your tests by setting server.error.include-binding-errors to always.
You can see a complete list of the properties that DevTools sets in the reference documentation.
Why message from #NotBlank is not displayed?
Controller API:
#PostMapping("/create-folder")
public SuccessResponse createFolder(Principal principal, #Valid #RequestBody CreateFolderRequest request) {
return historyService.createFolder(principal.getName(), request.getFolderName());
}
Request Body:
#Data
public class CreateFolderRequest {
#NotBlank(message = "Folder name is mandatory.")
private String folderName;
}
JSON Response:
{
"timestamp": "2020-11-18T11:24:19.769+00:00",
"status": 400,
"error": "Bad Request",
"message": "Validation failed for object='createFolderRequest'. Error count: 1",
"path": "/api/history/create-folder"
}
Packages:
Valid:
import javax.validation.Valid;
NotBlank:
import javax.validation.constraints.NotBlank;
There is no global exception handlers in the project.
#Valid throw an exception of MethodArgumentNotValidException you massege in #NotBlank is get throw inside exception detail which isn't return to the customer. you need to extract the messages so try adding this method to the controller.
#ResponseStatus(HttpStatus.BAD_REQUEST)
#ExceptionHandler(MethodArgumentNotValidException.class)
public Map<String, String> handleValidationExceptions(
MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getAllErrors().forEach((error) -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});
return errors;
}
above code read all errors inside the exception then get thier detail (filedName - errorMessage) and put them in list and then return the list to the clinet with 400 bad request status
I have a problem. I am using Spring Boot and sqlite3 DB. I tried to send data to the DB.
When I sent data to the DB I have this error:
{
"timestamp": "2019-02-12T12:39:40.413+0000",
"status": 400,
"error": "Bad Request",
"message": "JSON parse error: Cannot deserialize instance of `com.dar.darkozmetika.models.CategoryModel` out of START_ARRAY token; nested exception is com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot deserialize instance of `com.dar.darkozmetika.models.CategoryModel` out of START_ARRAY token\n at [Source: (PushbackInputStream); line: 1, column: 1]",
"path": "/api/cateogry/dar"
}
This is my controller:
RestController
#RequestMapping("api/cateogry/dar")
public class CategoryController {
#Autowired
private CategoryRepository categoryRepository;
#GetMapping
private List<CategoryModel> getAllCategory (){
System.out.println("sadad");
System.out.println("sadad" + this.categoryRepository.findAll());
return this.categoryRepository.findAll();
}
#PostMapping
#ResponseStatus(HttpStatus.OK)
public void create(#RequestBody CategoryModel bike) {
categoryRepository.save(bike);
}
#GetMapping("/{id}")
public CategoryModel getSpecificCategory(#PathVariable("id") long id) {
return null;//categoryRepository.getOne(id);
}
}
This is my model:
#Entity
#JsonIgnoreProperties({ "hibernateLazyInitializer", "handler" })
public class CategoryModel {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String categoryName;
private String categoryDescription;
private String imagePath;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getCategoryName() {
return categoryName;
}
I sent this data from Postman:
[
{
"id": 2,
"categoryName": "dsds",
"categoryDescription": "sdsd",
"imagePath": "Jsdsds"
}
]
Very interesting, I can get data from the DB without problems. This is return form my DB.
[
{
"id": 1,
"categoryName": "jeff#bikes.com",
"categoryDescription": "Globo MTB 29 Full Suspension",
"imagePath": "Jeff Miller"
}
]
Your request is not ok, you are sending an array of CategoryModel and the POST api/cateogry/dar receives just a CategoryModel. You should send just:
{
"id": 2,
"categoryName": "dsds",
"categoryDescription": "sdsd",
"imagePath": "Jsdsds"
}
My goal is to have a custom response body for validation errors. This is a very common case and I've read lots of posts/blogs/articles and I've even implemented this myself in the past. For some reason, I cannot figure this out.
I have this #RestControllerAdvice
#Slf4j
#RequiredArgsConstructor
#RestControllerAdvice
public class ErrorHandler extends ResponseEntityExceptionHandler {
private final MessageSource messageSource;
#Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex,
HttpHeaders headers, HttpStatus status, WebRequest request) {
ValidationErrorDTO validationError = ValidationErrorDTO.builder()
.fieldErrors(ex.getBindingResult().getFieldErrors().stream()
.map(fieldError -> FieldErrorDTO.builder()
.field(fieldError.getField())
.message(messageSource.getMessage(fieldError, LocaleContextHolder.getLocale()))
.build())
.collect(Collectors.toList()))
.build();
LOGGER.debug("W3MnsZ validation error: {}", validationError);
return new ResponseEntity<Object>(validationError, HttpStatus.BAD_REQUEST);
}
}
====
#ToString
#Builder
public class ValidationErrorDTO {
private final List<FieldErrorDTO> fieldErrors;
}
====
#ToString
#Builder
#Getter
#JsonInclude(Include.NON_NULL)
public class FieldErrorDTO {
private final String field;
private final String message;
}
My ErrorHandler.handleMethodArgumentNotValid() gets hit, but the actual response body returned to the client is not from my ValidationErrorDTO.
{
"timestamp": 1523115887261,
"status": 400,
"error": "Bad Request",
"exception": "org.springframework.web.bind.MethodArgumentNotValidException",
"errors": [
{
"codes": [
"NotEmpty.myDTO.field.another.lastly",
"NotEmpty.field.another.lastly",
"NotEmpty.lastly",
"NotEmpty.java.lang.String",
"NotEmpty"
],
"arguments": [
{
"codes": [
"myDTO.field.another.lastly",
"field.another.lastly"
],
"arguments": null,
"defaultMessage": "field.another.lastly",
"code": "field.another.lastly"
}
],
"defaultMessage": "may not be empty",
"objectName": "myDTO",
"field": "field.another.lastly",
"rejectedValue": null,
"bindingFailure": false,
"code": "NotEmpty"
}
],
"message": "Validation failed for object='myDTO'. Error count: 1",
"path": "/myPath"
}
I've figured out that what's happening is org.springframework.boot.autoconfigure.web.DefaultErrorAttributes is getting hit and somehow overriding my custom response body.
What do I do to allow my custom response body to be returned to the client?
Just figured it out.
ValidationErrorDTO.fieldErrors had no getter.
#ToString
#Builder
#Getter
public class ValidationErrorDTO {
#Singular
private final List<FieldErrorDTO> fieldErrors;
}
Now it works and we get response:
{
"fieldErrors": [
{
"field": "field.another.lastly",
"message": "may not be empty"
}
]
}
I'm trying to follow the documentation for Springfox Swagger to get Java Bean Validation to work (http://springfox.github.io/springfox/docs/current/#springfox-support-for-jsr-303), but they are not showing up in the Swagger UI.
This is my Spring Configuration:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
#EnableSwagger2
#Import({springfox.bean.validators.configuration.BeanValidatorPluginsConfiguration.class})
#Configuration
public class SwaggerConfig {
#Bean
public Docket docket() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.build();
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("My API")
.build();
}
}
This is my request mapping:
#ApiOperation(value = "Use to get token for internal applications")
#PostMapping(value = AuthUris.TOKEN)
public AuthResponse token(#Valid #RequestBody AuthRequest authRequest) {
// implementation omitted
}
This is my POJO:
#ApiModel
public class AuthRequest {
#ApiModelProperty
#NotNull
#Size(min = 4, max = 50)
private String username;
#NotNull
private String password;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
I expected the NotNull and Size annotations to be captured in the Swagger UI but they are not. Please help me understand how this should work. Thank you.
.
So thanks to https://stackoverflow.com/users/8012379/indra-basak I do see that they were working. However, I have to hover over the field to thinks like #Size. See the screenshot below.
If you are using springfox version 2.7.0, both #NotNull and #Size annotations should work.
Your #NotNull annotation is already working in the password field.
If #ApiModelProperty annotation is present for a field, it takes precedence over #NotNull annotation. It is the case with the username field. It shows up as optional because the required attribute of #ApiModelProperty annotation is set to false by default.
If you use springfox version 2.7.0 and don't use #ApiModelProperty annotation, the model will show up as:
Validation
For example, if you enter a username less than the minimum size of 4, you will get the following exception:
{
"timestamp": 1511550365198,
"status": 400,
"error": "Bad Request",
"exception": "org.springframework.web.bind.MethodArgumentNotValidException",
"errors": [
{
"codes": [
"Size.authRequest.username",
"Size.username",
"Size.java.lang.String",
"Size"
],
"arguments": [
{
"codes": [
"authRequest.username",
"username"
],
"arguments": null,
"defaultMessage": "username",
"code": "username"
},
50,
4
],
"defaultMessage": "size must be between 4 and 50",
"objectName": "authRequest",
"field": "username",
"rejectedValue": "s",
"bindingFailure": false,
"code": "Size"
}
],
"message": "Validation failed for object='authRequest'. Error count: 1",
"path": "/tokens"
}