I have a projectDTO which accepts a project name and start date. See below
#Data
#AllArgsConstructor
#NoArgsConstructor
public class ProjectDTO {
#NotBlank(message = "project name is required")
private String projectName;
#NotNull(message = "project start date is required")
#Pattern(regexp = "^\\d{4}\\-(0[1-9]|1[012])\\-(0[1-9]|[12][0-9]|3[01])$", message = "date format should be yyyy-MM-dd")
private LocalDate startDate;
}
Below is my ExceptionHandler class
#RestControllerAdvice
public class ApplicationExceptionHandler {
#ExceptionHandler(MethodArgumentNotValidException.class)
private ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex) {
List<String> errors = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.collect(Collectors.toList());
ApiException apiException = ApiException.builder()
.errors(errors)
.httpStatus(HttpStatus.BAD_REQUEST)
.timestamp(ZonedDateTime.now(ZoneId.of("Z")))
.build();
return new ResponseEntity<>(apiException, HttpStatus.BAD_REQUEST);
}
}
If I add a null value for the project name it validates as expected and give the custom error response as below.
{
"errors": [
"project name is required"
],
"httpStatus": "BAD_REQUEST",
"timestamp": "2023-02-17T05:06:08.9362836Z"
}
But if I provide a wrong format for the start date (e.g. - 2023/12-17) the validation is not working. Getting below error
{
"timestamp": "2023-02-17T05:21:07.004+00:00",
"status": 400,
"error": "Bad Request",
"path": "/api/projects"
}
In the console
Resolved [org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot deserialize value of type `java.time.LocalDate` from String "2023/02-17": Failed to deserialize java.time.LocalDate: (java.time.format.DateTimeParseException) Text '2023/02-17' could not be parsed at index 4; nested exception is com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type `java.time.LocalDate` from String "2023/02-17": Failed to deserialize java.time.LocalDate: (java.time.format.DateTimeParseException) Text '2023/02-17' could not be parsed at index 4<LF> at [Source: (PushbackInputStream); line: 5, column: 25] (through reference chain: com.theravado.donationservice.dto.ProjectRequest["projects"]->java.util.ArrayList[0]->com.theravado.donationservice.dto.ProjectDTO["startDate"])]
Can you give my some input on how to get this date format validation can be fixed for #Pattern so that I can output an informative error message like in project name?
Cheers
Edited
Thanks #times29. I feel like my validation is not applied at all for the start date here. exquisitely meantioning that "date format should be yyyy-MM-dd" rather than JSON parse error: Cannot deserialize value of type java.time.LocalDate from String "2023/02-17"
#Pattern(regexp = "^\\d{4}\\-(0[1-9]|1[012])\\-(0[1-9]|[12][0-9]|3[01])$", message = "date format should be yyyy-MM-dd")
You need a separate #ExceptionHandler for the HttpMessageNotReadableException. Your current #ExceptionHandler(MethodArgumentNotValidException.class) will only handle MethodArgumentNotValidException exceptions, but when a HttpMessageNotReadableException occurrs like in your case, it won't be handled by the handleMethodArgumentNotValid method.
#ExceptionHandler(HttpMessageNotReadableException.class)
private ResponseEntity<Object> handleHttpMessageNotReadableException(HttpMessageNotReadableException ex) {
List<String> errors = ...; // Extract info from the exception
ApiException apiException = ApiException.builder()
.errors(errors)
.httpStatus(HttpStatus.BAD_REQUEST)
.timestamp(ZonedDateTime.now(ZoneId.of("Z")))
.build();
return new ResponseEntity<>(apiException, HttpStatus.BAD_REQUEST);
}
Actually the issue was #Pattern cannot be used for LocalDate. When I changed the type to String in my projectDTO this started working
#NotNull(message = "project start date is required")
#Pattern(regexp = "^\\d{4}\\-(0[1-9]|1[012])\\-(0[1-9]|[12][0-9]|3[01])$", message = "date format should be yyyy-MM-dd")
private String startDate;
Related
I am using org.springframework.boot:spring-boot-starter-validation:2.7.0(which in turn uses hibernate validator) to validate user input to rest controller.
I am using Spring Boot Web Starter (2.7.0) based project with #RestController annotation
My #GetMapping method is something like below -
#GetMapping(path = "/abcservice")
public Object abcService(
#RequestParam(value = "accountId", required = true) String accountId,
#Valid #RequestParam(value = "offset", required = false, defaultValue = "0") int offset,
#Valid #RequestParam(value = "limit", required = false, defaultValue = "10000") int limit
) throws Exception {
My problem is - I want the user to know about any input validation errors so they can correct and retry. But the framework is just giving 400 status code with below message.
{
"timestamp": "2022-08-03T16:10:14.554+00:00",
"status": 400,
"error": "Bad Request",
"path": "/somepath/abcservice"
}
On the server side the request is logged in warn.
2022-08-03 21:40:14.535 WARN 98866 --- [nio-8080-exec-1]
.w.s.m.s.DefaultHandlerExceptionResolver : Resolved
[org.springframework.web.method.annotation.MethodArgumentTypeMismatchException:
Failed to convert value of type 'java.lang.String' to required type
'int'; nested exception is java.lang.NumberFormatException: For input
string: "0s"]
I want this above error message --> Failed to convert value of type 'java.lang.String' to required type 'int'; nested exception is java.lang.NumberFormatException: For input string: "0s" also to be passed on to user. Is there a easy configuration based way to achieve.
I think i can add a ControllerAdvice to handle this exception and include this message in the response from handler method. But this will be a couple of lines of code. Is there an even simpler way than the ControllerAdvice approach.
Similarly if the client don't pass the mandatory accountId param, the client is just getting the same 400 response as above. No details or hints to the client about what wrong they are doing or how they can fix it.. but on the server side i can see below warn log.
2022-08-03 21:59:20.195 WARN 235 --- [nio-8080-exec-3]
.w.s.m.s.DefaultHandlerExceptionResolver : Resolved
[org.springframework.web.bind.MissingServletRequestParameterException:
Required request parameter 'accountId' for method parameter type
String is not present]
I want the client to know about this error/exception. Nothing secret here to hide (atleast in my case).
Edit - found this config -
server.error.include-message=always
Now the issue is, bad requests are sent with 500 status code, I want them to sent as 400. Then this Q is solved.
Validations made by #Valid return with 500 Status Code. Is there anyway to tell the server to return 400 response when validations fail (without using ControllerAdvice).
If you wish to test-- you can try -->
Annotate controller with #Validated.
And execute below method and you will see 500 error but would want this to be 400.
#GetMapping("/test")
public void test(#Valid #RequestParam(value = "studentId", required = false)
#Min(value=0, message="Can not be less than 0") #Max(value=200, message="Can not be above 200") Long studentId ) {
System.out.println("hit: ");
}
And hit - http://localhost:9099/test?studentId=400
The spring in-built solution without global exception handler and with minimal config is by adding the below property in the application.properties.
server.error.include-binding-errors=always
The above property can have three values:
always ----> All api's in the app will always return well defined validation error message response.
on-param ----> All api's in the app will conditionally return well defined validation error message response based on input request param field "errors"
never ---> to disable this feature.
Example Github Project Reference
Demo Test:
package com.example.demo;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
#SpringBootApplication
#RestController
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
#PostMapping("/test")
public void test(#Valid #RequestBody Student student) {
System.out.println("studentData: " + student);
}
}
class Student {
#NotNull(message = "firstName cannot be null")
private String firstName;
private String lastName;
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;
}
#Override
public String toString() {
return "Student [firstName=" + firstName + ", lastName=" + lastName + "]";
}
}
Request:
{
"firstName": null,
"lastName" : "sai"
}
Response: (with HTTP response code = 400)
{
"timestamp": "2022-08-04T05:23:58.837+00:00",
"status": 400,
"error": "Bad Request",
"errors": [
{
"codes": [
"NotNull.student.firstName",
"NotNull.firstName",
"NotNull.java.lang.String",
"NotNull"
],
"arguments": [
{
"codes": [
"student.firstName",
"firstName"
],
"arguments": null,
"defaultMessage": "firstName",
"code": "firstName"
}
],
"defaultMessage": "firstName cannot be null",
"objectName": "student",
"field": "firstName",
"rejectedValue": null,
"bindingFailure": false,
"code": "NotNull"
}
],
"path": "/test"
}
Use #expection handler and controller advice this help to handle your issue
seems like by default Spring will return a message of:
{
"timestamp": "2019-01-17T16:12:45.977+0000",
"status": 500,
"error": "Internal Server Error",
"message": "Error processing the request!",
"path": "/my-endpoint-with-exceptions"
}
currently the app is using #RestControllerAdvice with an #ExceptionHandler on each exception. The in each method it uses a ResponseEntity
#ExceptionHandler(GenericException.class)
public ResponseEntity<String> exceptionHandler(GenericException ex){
return new ResponseEntity<>(ex.getMessage,HttpStatus.BAD_REQUEST)
}
additionally seems like over time there have been any number of classes which do about the same time as the default which are used.
So would rather use the default Spring JSON however of course do not want to impact currently running code. So my question is for just the GenericException to return the default Spring JSON?
I did try to use ResponseStatusException which did return the JSON but for whatever reason would only return a INTERNAL_SERVER_ERROR (500) status even when setting the value in the argument.
You can define your own error response and return it from the exception handler method.
Something like this:
Model:
#Builder
public class ErrorResponse {
private int status;
private String error;
private String message;
private String path;
private long timestamp;
}
Handler:
#ExceptionHandler(GenericException.class)
public ResponseEntity<ErrorResponse> exceptionHandler(GenericException ex){
ErrorResponse errorResponse = ErrorResponse.builder()
.message(ex.getMessage())
.status(HttpStatus.BAD_REQUEST.value())
.error(HttpStatus.BAD_REQUEST.getReasonPhrase())
.build();
return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
}
in my controller I have an endpoint:
#GetMapping(value = SUMMARY_URL, produces = "application/json")
public DailyReportSummary getSummaryOfDailyReports(
#RequestParam(name = "from", required = false,defaultValue = "10-10-2017 ") #DateTimeFormat(pattern = "dd-MM-yyyy") LocalDateTime from,
#RequestParam(name = "to", required = false,defaultValue = "10-10-2019 ") #DateTimeFormat(pattern = "dd-MM-yyyy") LocalDateTime to) {
List<DailyReport> summary = statisticService.findByDateToSummary(from, to);
DailyReportSummary dailyReportSummary = new DailyReportSummary(summary);
I though that all is ok, but I have this error:
There was an unexpected error (type=Bad Request, status=400).
Failed to convert value of type 'java.lang.String' to required type 'java.time.LocalDateTime'; nested exception is org.springframework.core.convert.ConversionFailedException:
Failed to convert from type [java.lang.String] to type [#org.springframework.web.bind.annotation.RequestParam
#org.springframework.format.annotation.DateTimeFormat java.time.LocalDateTime] for value '10-10-2017 '; nested exception is java.lang.IllegalArgumentException: Parse attempt failed for value [10-10-2017 ]
What is wrong with this? I trying this solve this, but nothing works.
edit: with deleted iso still errors :(
Since the pattern dd-MM-yyyy doesn't have time part you need to use LocalDate
#GetMapping(value = SUMMARY_URL, produces = "application/json")
public DailyReportSummary getSummaryOfDailyReports(
#RequestParam(name = "from", required = false, defaultValue = "10-10-2017") #DateTimeFormat(pattern = "dd-MM-yyyy") LocalDate from,
#RequestParam(name = "to", required = false, defaultValue = "10-10-2019") #DateTimeFormat(pattern = "dd-MM-yyyy") LocalDate to) {
My url is http://localhost:8090/employee/?emp_id=1551&name=
I am using Spring boot for designing REST application. I have used #RequestMapping and #RequestParam annotation for get resource. When I pass empty value to request parameter (for eg. name = ), I get below validation response(actual output section below).
However I wanted to override this output to display customized error response as below(expected section below).
How can I achieve this? How to avoid Spring's auto validation for input parameters in Get request?
Output
======
{
"timestamp": 1511144660708,
"status": 400,
"error": "Bad Request",
"message": "Required String parameter 'name' is not present",
"path": "/employee"
}
Expected
========
{
"errors":[
{
"id":"123144",
"detail": "invalid user input"
"status": "400"
}
]
}
Following sample code demonstrates how to customize error message for exception handling.
Create 2 POJOs for your customized response body.
Implement 1 method to catch the MissingServletRequestParameterException exception with #ExceptionHandler annotation for missing paramters.
Generate the response as you expected.
Class: ResponseProperty.java
public class ResponseProperty {
private int id;
private String detail;
private int status;
//getters and setters produced by IDE
}
Class: ResponsePOJO.java
public class ResponsePOJO {
List<ResponseProperty> errors;
public List<ResponseProperty> getErrors() {
return errors;
}
public void setErrors(List<ResponseProperty> errors) {
this.errors = errors;
}
}
Method: handleMethodArgumentTypeMismatch
#ExceptionHandler({ MissingServletRequestParameterException.class })
public ResponseEntity<Object> handleMethodArgumentTypeMismatch(MissingServletRequestParameterException ex) {
ResponseProperty property = new ResponseProperty();
property.setId(123144);
property.setDetail("invalid user input");
property.setStatus(400);
ResponsePOJO responsePOJO = new ResponsePOJO();
List<ResponseProperty> propertyList = new ArrayList<ResponseProperty>();
propertyList.add(property);
responsePOJO.setErrors(propertyList);
return new ResponseEntity<Object>(responsePOJO, HttpStatus.BAD_REQUEST);
}
If you visit the endpoint /employee without required parameter, then you are going to see the response as follows:
Http Response
{
"errors": [
{
"id": 123144,
"detail": "invalid user input",
"status": 400
}
]
}
Hope this helps you! :)
UPDATE
If you want to get the request ID from header named requestId for response, you can use WebRequest to get this information as follows:
#ExceptionHandler({ MissingServletRequestParameterException.class })
public ResponseEntity<Object> handleMethodArgumentTypeMismatch(MissingServletRequestParameterException ex,
WebRequest request) {
ResponseProperty property = new ResponseProperty();
property.setId(Integer.valueOf(request.getHeader("requestId")));
...
}
I'm using Spring Boot 1.4 and the following works. I have this #Bean defined:
#Bean
public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() {
ObjectMapper mapper = new ObjectMapper();
mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd"));
mapper.registerModule(new JodaModule());
return new MappingJackson2HttpMessageConverter(mapper);
}
And I have this DTO defined:
public class ReportRequest implements Serializable {
private LocalDate startDate;
private LocalDate endDate;
// additional fields and getters/setters omitted
}
I'm submitting this data into a controller with #RequestBody ReportRequest with the following json in the body of the request:
{
"startDate": "2016-09-01",
"endDate": "2016-09-12"
}
Works great. However, I need to also include the time. So I changed everything to look like this:
mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
private LocalDateTime startDate;
private LocalDateTime endDate;
{
"startDate": "2016-09-01 02:00:00",
"endDate": "2016-09-12 23:59:59"
}
This does not work. I'm getting:
Could not read document: Invalid format: \"2016-09-01 02:00:00\" is malformed at \" 02:00:00\" (through reference chain: com.hightouchinc.dto.ReportRequest[\"startDate\"]); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Invalid format: \"2016-09-01 02:00:00\" is malformed at \" 02:00:00\" (through reference chain: com.hightouchinc.dto.ReportRequest[\"startDate\"])
Update: I modified the following:
mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"));
And can send "2016-09-01T02:00:00" and it works. But removing the T from both continues to break.
It doesn't appear that LocalDateTimeDeserializer respects the value passed to setDateFormat(), as can be seen in JodaModule:
addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer());
What you can do is override the default deserializer in the module before registering it:
JodaModule jodaModule = new JodaModule();
JacksonJodaDateFormat format = new JacksonJodaDateFormat("yyyy-MM-dd HH:mm:ss");
jodaModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(format)));
mapper.registerModule(jodaModule);
and that should use the correct pattern to deserialize your LocalDateTime instances.