I have the following controller method:
@RequestMapping(value="/map/update", method=RequestMethod.POST, produces = "application/json; charset=utf-8") @ResponseBody public ResponseEntityWrapper updateMapTheme( HttpServletRequest request, @RequestBody @Valid List<CompanyTag> categories, HttpServletResponse response ) throws ResourceNotFoundException, AuthorizationException { ... }
CompanyTag is defined this way:
public class CompanyTag { @StringUUIDValidation String key; String value; String color; String icon; Icon iconObj; public String getKey() { return key; } public void setKey(String key) { this.key = key; } ... }
The problem is that validation is not triggered, the CompanyTag list is not validated, the "StringUUIDValidation" validator is never called.
If I remove the List and only try to send a single CompanyTag, i.e. instead of:
@RequestBody @Valid List<CompanyTag> categories,
use:
@RequestBody @Valid CompanyTag category,
it works as expected, so apparently Spring does not like to validate lists of things (tried with array instead, that did not work either).
Anybody have any idea what's missing?
public class ValidList<E> implements List<E> { @Valid private List<E> list; public ValidList() { this. list = new ArrayList<E>(); } public ValidList(List<E> list) { this. list = list; } // Bean-like methods, used by javax.
The @RequestBody annotation is used to bind the HTTP request body with a domain object in the method parameter and also this annotation internally uses the HTTP Message converter to convert the body of the HTTP request to a domain object.
The @Valid annotation ensures the validation of the whole object. Importantly, it performs the validation of the whole object graph. However, this creates issues for scenarios needing only partial validation. On the other hand, we can use @Validated for group validation, including the above partial validation.
I found another approach that works. The basic problem is that you want to have a list as your input payload for your service, but javax.validation won't validate a list, only a JavaBean. The trick is to use a custom list class that functions as both a List and a JavaBean:
@RequestBody @Valid List<CompanyTag> categories
Change to:
@RequestBody @Valid ValidList<CompanyTag> categories
Your list subclass would look something like this:
public class ValidList<E> implements List<E> { @Valid private List<E> list; public ValidList() { this.list = new ArrayList<E>(); } public ValidList(List<E> list) { this.list = list; } // Bean-like methods, used by javax.validation but ignored by JSON parsing public List<E> getList() { return list; } public void setList(List<E> list) { this.list = list; } // List-like methods, used by JSON parsing but ignored by javax.validation @Override public int size() { return list.size(); } @Override public boolean isEmpty() { return list.isEmpty(); } // Other list methods ... }
I tried to use Paul's method in my project, but some people said it's too complex. Not long after that, I find another easy way which works like code below:
@Validated @RestController @RequestMapping("/parent") public class ParentController { private FatherRepository fatherRepository; /** * DI */ public ParentController(FatherRepository fatherRepository) { this.fatherRepository = fatherRepository; } @PostMapping("/test") public void test(@RequestBody @Valid List<Father> fathers) { } }
It works and easy to use. The key point is the @Valiated annotation on the class. Btw, it's springBootVersion = '2.0.4.RELEASE' that I use.
As discussed in comments, exceptions can be handled like code below:
@RestControllerAdvice @Component public class ControllerExceptionHandler { /** * handle controller methods parameter validation exceptions * * @param exception ex * @return wrapped result */ @ExceptionHandler @ResponseBody @ResponseStatus(HttpStatus.OK) public DataContainer handle(ConstraintViolationException exception) { Set<ConstraintViolation<?>> violations = exception.getConstraintViolations(); StringBuilder builder = new StringBuilder(); for (ConstraintViolation<?> violation : violations) { builder.append(violation.getMessage()); break; } DataContainer container = new DataContainer(CommonCode.PARAMETER_ERROR_CODE, builder.toString()); return container; } }
Taking http status code as representing network is ok and only first violation message is returned here. You may change it to satisfy customized requirements.
With @Validated on class level, parameters of methods are validated by what called method-level validation in spring boot, which is not only worked for controllers, but any bean the IOC
container managed.
By the way, the methods in method level validation (short as validation A) is enhanced by
while the typical spring boot controller methods validation (short as validation B) is processed in
Both of them lead the actual validation operation to org.hibernate.validator.internal.engine.ValidatorImpl
by default, but the methods they call are different, which leads to the differences in validation logic.
MethodValidationInterceptor
call validateParameters
method in ValidatorImpl
RequestResponseBodyMethodProcessor
call validate
method in ValidatorImpl
They are different methods with different functions, so lead to different results in validation A/B, the typical point is the validation of list object:
The JSR-303 defines functions of the methods we discussed above.
validate
method is explained in the validation method part, and the implementation must obey the logic defined in validation routine, in which it states that it will execute all the constraint validation for all reachable fields of the object, this is why element of List
object (or other collection instance) cannot be validated via this method - the elements of the collection are not fields of the collection instance.
But validateParameters
, JSR-303 actually doesn't treat it as main topic and put it in Appendix C. Proposal for method-level validation
. It provides some description:
The constraints declarations evaluated are the constraints hosted on the parameters of the method or constructor. If @Valid is placed on a parameter, constraints declared on the object itself are considered. validateReturnedValue evaluates the constraints hosted on the method itself. If @Valid is placed on the method, the constraints declared on the object itself are considered. public @NotNull String saveItem(@Valid @NotNull Item item, @Max(23) BigDecimal price) In the previous example, - item is validated against @NotNull and all the constraints it hosts - price is validated against @Max(23) - the result of saveItem is validated against @NotNull
and exclaim that Bean Validation providers are free to implement this proposal as a specific extension
. As far as I know, the Hibernate Validation
project implements this method, makes constraints works on the object itself, and element of collection object.
I don't know why the spring framework guys call validate
in RequestResponseBodyMethodProcessor
, makes lots of related questions appeare in stackoverflow. Maybe it's just because http post body data usually is a form data, and can be represented by a java bean naturally. If it's me, I'll call the validateParametes
in RequestResponseBodyMethodProcessor
for easy use.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With