Jackson deserialization errorhandling in spring-framework - spring

I'm looking for a clean way to handle Jackson Deserialization errors for REST web requests.
More precisely: I have an Enum in a incoming DTO object, mapped from JSON. But if the user sends a wrong value, a 400 Bad Request is returned. I would like to return a 422 Unprocessable Entity with a correct message.
One option would be to accept a String, and use bean validation. However, it's not possible to pass all enum values as a list to the annotation (not a constant), so I would need to pass all enum values separately and keep them up to date. This will be very error prone over the whole application. I'm looking for a more structural way to handle this.

I solved this by using a String in the DTO and using a public #interface EnumValueas annotation.
The EnumValue:
#ReportAsSingleViolation
#Documented
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = EnumValueValidator.class)
#Target(ElementType.FIELD)
public #interface EnumValue {
Class<? extends Enum> value();
String message() default "The input contains validation errors.";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}
The validator:
public class EnumValueValidator implements ConstraintValidator<EnumValue, String> {
private Class<? extends Enum> enumClass;
private String message;
#Override
public void initialize(final EnumValue constraintAnnotation) {
this.enumClass = constraintAnnotation.value();
this.message = constraintAnnotation.message();
}
#Override
public boolean isValid(final String value, final ConstraintValidatorContext context) {
boolean valid = false;
for (final Enum enumValue : enumClass.getEnumConstants()) {
if (enumValue.name().equals(value)) {
valid = true;
}
}
if (!valid) {
context.buildConstraintViolationWithTemplate(message) //
.addConstraintViolation();
}
return valid;
}
}

Related

Spring custom validator with dependencies on other fields

We are using spring custom validator for our request object used in our controller endpoint. We implemented it the same way as how its done in the link below:
https://www.baeldung.com/spring-mvc-custom-validator
The problem we are facing is, it can't work if the particular field has dependencies on other input fields as well. For example, we have the code below as the request object for our controller endpoint:
public class FundTransferRequest {
private String accountTo;
private String accountFrom;
private String amount;
#CustomValidator
private String reason;
private Metadata metadata;
}
public class Metadata {
private String channel; //e.g. mobile, web, etc.
}
Basically #CustomValidator is our custom validator class and the logic we want is, if the supplied channel from Metadata is "WEB". The field "reason" of the request won't be required. Else, it will be required.
Is there a way to do this? I've done additional research and can't see any that handles this type of scenario.
Obviously if you need access to multiple fields in your custom validator, you have to use a class-level annotation.
The same very article you mentioned has an example of that: https://www.baeldung.com/spring-mvc-custom-validator#custom-class-level-validation
In your case it might look something like this:
#Constraint(validatedBy = CustomValidator.class)
#Target({ ElementType.TYPE })
#Retention(RetentionPolicy.RUNTIME)
public #interface CustomValidation {
String message() default "Reason required";
String checkedField() default "metadata.channel";
String checkedValue() default "WEB";
String requiredField() default "reason";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
package com.example.demo;
import org.springframework.beans.BeanWrapperImpl;
import org.springframework.stereotype.Component;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
/*
If the supplied channel from Metadata is "WEB". The field "reason" of the request won't be required.
Else, it will be required.
*/
#Component
public class CustomValidator implements ConstraintValidator<CustomValidation, Object> {
private String checkedField;
private String checkedValue;
private String requiredField;
#Override
public void initialize(CustomValidation constraintAnnotation) {
this.checkedField = constraintAnnotation.checkedField();
this.checkedValue = constraintAnnotation.checkedValue();
this.requiredField = constraintAnnotation.requiredField();
}
#Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
Object checkedFieldValue = new BeanWrapperImpl(value)
.getPropertyValue(checkedField);
Object requiredFieldValue = new BeanWrapperImpl(value)
.getPropertyValue(requiredField);
return checkedFieldValue != null && checkedFieldValue.equals(checkedValue) || requiredFieldValue != null;
}
}
And the usage will be:
#CustomValidation
public class FundTransferRequest {
...
or with parameters specified:
#CustomValidation(checkedField = "metadata.channel",
checkedValue = "WEB",
requiredField = "reason",
message = "Reason required")
public class FundTransferRequest {
...

Springboot #Valid annotation with String object

So, I have the following controller method:
#RequestMapping(path = "/{application}/users", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
public MyObject getUsers(#RequestParam("itemId") String itemId, #PathVariable("application") String application) {
return userService.get(itemId, application);
}
I would like to check if the request parameter itemId exists in the related application (in the path).
My first idea was to create a validator :
#RequestMapping(path = "/{application}/users", method = RequestMethod.GET, produces =
MediaType.APPLICATION_JSON_VALUE)
#CheckItemId
public MyObject getUsers(#RequestParam("itemId") String itemId, #PathVariable("application") String application) {
return userService.get(itemId, application);
}
CheckItemId.java :
#Target({METHOD})
#Retention(RUNTIME)
#Constraint(validatedBy = CheckItemIdValidator.class)
#Documented
public #interface CheckItemId {
String message() default "error";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
CheckItemIdValidator.java :
#SupportedValidationTarget(ValidationTarget.PARAMETERS)
public class CheckItemIdValidator implements ConstraintValidator<CheckItemId, Object[]>{
#Override
public boolean isValid(Object[] arg0, ConstraintValidatorContext arg1) {
String itemId= (String) arg0[0];
String application = (String) arg0[1];
// Logic business ...
return true;
}
}
This implementation works well, I managed to get the values itemId and application in the validator. I can now do my verification.
I was wondering if there is a better way to do something like that? Since I handle an array of Object, I need to cast it to String and If I change the parameters order, I will not get the same values since I need to use arg0[0] and arg0[1].
Thank you !
You can use spring validation library. Add #Valid on controller level. Then add #NotBlank on method level as below.
getUsers(#RequestParam("itemId") #NotBlank String itemId)

How do I validate a #QueryParam?

I've got a simple REST resource which accepts a couple of query parameters. I'd like to validate one of these parameters, and came across ConstraintValidator for this purpose. The REST resource expects the query param territoryId to be a UUID, so I'd like to validate that it indeed is a valid UUID.
I've created an #IsValidUUID annotation, and a corresponding IsValidUUIDValidator (which is a ConstraintValidator). With what I have now, nothing gets validated and getSuggestions accepts anything I throw at it. So clearly I'm doing something wrong.
What am I doing wrong?
The REST resource now looks like this :
#Component
#Path("/search")
public class SearchResource extends AbstractResource {
#GET
#Path("/suggestions")
#Produces(MediaType.APPLICATION_XML)
public Response getSuggestions(
#QueryParam("phrase") List<String> phrases,
#IsValidUUID #QueryParam("territoryId") String territoryId) {
[...]
}
}
IsValidUUID
#Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})
#Retention(RetentionPolicy.RUNTIME)
#Documented
#Constraint(validatedBy = {IsValidUUIDValidator.class})
public #interface IsValidUUID {
String message() default "Invalid UUID";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
IsValidUUIDValidator
public class IsValidUUIDValidator implements ConstraintValidator<IsValidUUID, String> {
#Override
public void initialize(IsValidUUID constraintAnnotation) {
}
#Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null) {
return true;
}
try {
UUID.fromString(value);
return true;
} catch (Exception e) {
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate("The provided UUID is not valid")
.addConstraintViolation();
return false;
}
}
}
You need to set the supported targets on IsValidUUID, using the following annotation.
#SupportedValidationTarget(ValidationTarget.ANNOTATED_ELEMENT)
or
#SupportedValidationTarget(ValidationTarget.PARAMETERS)
Edit:
Sorry, I wasn't able to make it work either on a RequestParam directly. However, if you can, try creating a POJO that you can bind your request parameters to and annotate the binding field with your constraint instead. This worked for me.
public class MyModel {
#IsValidUUID
private String territoryId;
public String getTerritoryId() {
return territoryId;
}
public void setTerritoryId(String territoryId) {
this.territoryId = territoryId;
}
}
#GET
#Path("/suggestions")
#Produces(MediaType.APPLICATION_XML)
public Response getSuggestions(
#QueryParam("phrase") List<String> phrases,
#Valid #ModelAttribute MyModel myModel) {
[...]
}

Spring MVC Validation for list and reporting the invalid value

I have a list of strings which should be of a specific format. I need to return the error message with the strings which are not of the format specified. How to do this with spring validation(I am using the hibernate validator).
The annotation:
#Documented
#Retention(RUNTIME)
#Target({FIELD, METHOD})
#Constraint(validatedBy = HostsValidator.class)
public #interface HostsConstraint {
String message();
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
The implementation:
public class HostsValidator implements ConstraintValidator<HostsConstraint, List<String>>{
#Override
public void initialize(OriginHostsConstraint constraintAnnotation) {
}
#Override
public boolean isValid(List<String> strings, ConstraintValidatorContext context) {
for (String s : strings) {
if (!s.matches("[0-9]+") {
//How do I say: Invalid string <s> ?
return false;
}
}
}
}
The usage:
public class Test {
#HostsConstraint(message="Invalid string ")
private List<String> hosts;
}
Using validatedValue will give the entire list.
Use JSR 380 validation, it allows container element constraints.
Here is a link to the container element section in the Hibernate Validator 6.0.6.FINAL Document
I think I found a solution but it is coupled to hibernate validator. May be it is even a hacky implementation.
The usage:
public class Test {
#HostsConstraint(message="Invalid string : ${invalidStr}")
private List<String> hosts;
}
The implementation
public class HostsValidator implements ConstraintValidator<HostsConstraint, List<String>>{
#Override
public void initialize(OriginHostsConstraint constraintAnnotation) {}
#Override
public boolean isValid(List<String> strings, ConstraintValidatorContext context) {
for (String s : strings) {
if (!s.matches("[0-9]+") {
ConstraintValidatorContextImpl contextImpl =
(ConstraintValidatorContextImpl) context
.unwrap(HibernateConstraintValidatorContext.class);
contextImpl.addExpressionVariable("invalidStr", s);
return false;
}
}
}
}

Custom JSR 303 validation is not invoked

My custom JSR 303 validation is not getting invoked. Here is my code
my spring config has
<mvc:annotation-driven />
My controller's handler method:
#RequestMapping(value="update", method = RequestMethod.POST ,
consumes="application/json" ,
produces="application/json"))
#ResponseBody
public String update(#Valid #RequestBody MyBean myBean){
return process(myBean);
}
MyBean (annotated with ValidMyBeanRequest):
#ValidMyBeanRequest
public class MyBean {
private int id;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
}
ValidMyBeanRequest annotaion:
#Target({ TYPE })
#Retention(RUNTIME)
#Documented
#Constraint(validatedBy = {MyBeanValidator.class})
public #interface ValidMyBeanRequest {
String message() default "{validMyBeanRequest.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
MyBeanValidator class:
public class MyBeanValidator implements
ConstraintValidator<ValidMyBeanRequest, MyBean> {
#Override
public void initialize(ValidMyBeanRequest constraintAnnotation) {
// TODO Auto-generated method stub
}
#Override
public boolean isValid(MyBean myBean, ConstraintValidatorContext context) {
boolean isValid = true;
int id = myBean.getId();
if(id == 0){
isValid = false;
}
return isValid;
}
}
My http POST request has below JSON data:
{id:100}
The problem is MyBeanValidator's isValid is not getting invoked. I am using Spring 3.1.0 and HibernateValidator is in classpath.
Please see what I am missing??
Update: Updated handler method to include POST request type and consumes, produces values. Also included my http request with JSON data.
Assuming that you do get model correctly, in this case you are doing everything right, except one thing: you need to handle your validation's result manually.
For achieving this you need to add BindingResult object into list of your handler parameters, and then process validation constraints in the way you would like:
#RequestMapping(value="update")
#ResponseBody
public String update(#Valid #ModelAttribute #RequestBody MyBean myBean, BindingResult result) {
if (result.hasErrors()){
return processErrors(myBean);
}
return process(myBean);
}

Resources