What's the simplest approach to validating a complex JSON object being passed into a GET REST contoller in spring boot that I am mapping with com.fasterxml.jackson.databind.ObjectMapper?
Here is the controller:
@RestController
@RequestMapping("/products")
public class ProductsController {
@GetMapping
public ProductResponse getProducts(
@RequestParam(value = "params") String requestItem
) throws IOException {
final ProductRequest productRequest =
new ObjectMapper()
.readValue(requestItem, ProductRequest.class);
return productRetriever.getProductEarliestAvailabilities(productRequest);
}}
DTO request object I want to validate:
public class ProductRequest {
private String productId;
public String getProductId() {
return productId;
}
public void setProductId(String productId) {
this.productId = productId;
}}
I was thinking of using annotations on the request DTO however when I do so, they are not triggering any type of exceptions, i.e. @NotNull. I've tried various combinations of using @Validated at the controller as well as @Valid in the @RequestParam and nothing is causing the validations to trigger.
In my point of view, Hibernate Bean Validator
is probably one of the most convenient methods to validate the annotated
fields of a bean anytime and anywhere. It's like setup
and forget
I followed the instructions in the documentation given here
I use Gradle so, I am going to add the required dependencies as shown below
// Hibernate Bean validator
compile('org.hibernate:hibernate-validator:5.2.4.Final')
I setup a bean validator interface as described in the documentation and then use this to validate everything that is annotated
public interface CustomBeanValidator {
/**
* Validate all annotated fields of a DTO object and collect all the validation and then throw them all at once.
*
* @param object
*/
public <T> void validateFields(T object);
}
Implement the above interface as follow
@Component
public class CustomBeanValidatorImpl implements CustomBeanValidator {
ValidatorFactory valdiatorFactory = null;
public CustomBeanValidatorImpl() {
valdiatorFactory = Validation.buildDefaultValidatorFactory();
}
@Override
public <T> void validateFields(T object) throws ValidationsFatalException {
Validator validator = valdiatorFactory.getValidator();
Set<ConstraintViolation<T>> failedValidations = validator.validate(object);
if (!failedValidations.isEmpty()) {
List<String> allErrors = failedValidations.stream().map(failure -> failure.getMessage())
.collect(Collectors.toList());
throw new ValidationsFatalException("Validation failure; Invalid request.", allErrors);
}
}
}
The ValidationsFatalException
I used above is a custom exception class that extends RuntimeException
. As you can see I am passing a message and a list of violations
in case the DTO has more than one validation error.
public class ValidationsFatalException extends RuntimeException {
private String message;
private Throwable cause;
private List<String> details;
public ValidationsFatalException(String message, Throwable cause) {
super(message, cause);
}
public ValidationsFatalException(String message, Throwable cause, List<String> details) {
super(message, cause);
this.details = details;
}
public List<String> getDetails() {
return details;
}
}
In order to test whether this is working or not, I literally used your code to test and here is what I did
CustomBeanValidator
and trigger it's validateFields
method passing the productRequest
into it as shown below ProductRequest
class as shown above productId
with @NotNull
and @Length(min=5, max=10)
Postman
to make a GET
request with a params
having a value that is url-encoded
json body Assuming that the CustomBeanValidator
is autowired in the controller, trigger the validation as follow after constructing the productRequest
object.
beanValidator.validateFields(productRequest);
The above will throw exception if any violations based on annotations used.
As mentioned in the title, I use ExceptionController
in order to handle the exceptions in my application.
Here is how the skeleton of my exception handler
where the ValidationsFatalException
maps to and then I update the message and set my desired status code based on exception type and return a custom object (i.e. the json you see below)
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler({SomeOtherException.class, ValidationsFatalException.class})
public @ResponseBody Object handleBadRequestExpection(HttpServletRequest req, Exception ex) {
if(ex instanceof CustomBadRequestException)
return new CustomResponse(400, HttpStatus.BAD_REQUEST, ex.getMessage());
else
return new DetailedCustomResponse(400, HttpStatus.BAD_REQUEST, ex.getMessage(),((ValidationsFatalException) ex).getDetails());
}
Raw params = {"productId":"abc123"}
Url encoded parmas = %7B%22productId%22%3A%22abc123%22%7D
Final URL: http://localhost:8080/app/product?params=%7B%22productId%22%3A%22abc123%22%7D
Result: All good.
Raw params = {"productId":"ab"}
Url encoded parmas = %7B%22productId%22%3A%22ab%22%7D
Final URL: http://localhost:8080/app/product?params=%7B%22productId%22%3A%22ab%22%7D
Result:
{
"statusCode": 400,
"status": "BAD_REQUEST",
"message": "Validation failure; Invalid request.",
"details": [
"length must be between 5 and 10"
]
}
You can expand the Validator
implementation to provide a mapping of field vs message
error message.