Validation of a list of objects in Spring

后端 未结 12 1209
無奈伤痛
無奈伤痛 2020-11-28 05:30

I have the following controller method:

@RequestMapping(value=\"/map/update\", method=RequestMethod.POST, produces = \"application/json; charset=utf-8\")
@Re         


        
相关标签:
12条回答
  • 2020-11-28 05:34

    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 ...
    }
    
    0 讨论(0)
  • 2020-11-28 05:39

    1 TL;DR

    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.

    2 Exception handling

    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.

    3 How it works (code part)

    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.

    • Spring boot docs about method level validation
    • A more detailed blog with test about method level validation

    By the way, the methods in method level validation (short as validation A) is enhanced by

    • org.springframework.validation.beanvalidation.MethodValidationInterceptor

    while the typical spring boot controller methods validation (short as validation B) is processed in

    • org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor

    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:

    • A triggers constraint check on element of collection object while B not

    4 How it works (specification part)

    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.

    5 Some complain

    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.

    0 讨论(0)
  • 2020-11-28 05:39

    @Paul Strack's great solution mixed with Lombok magic:

    @Data
    public class ValidList<E> implements List<E> {
        @Valid
        @Delegate
        private List<E> list = new ArrayList<>();
    }
    

    Usage (swap List for ValidList):

    public ResponseEntityWrapper updateMapTheme(
            @RequestBody @Valid ValidList<CompanyTag> categories, ...)
    

    (Needs Lombok, but if you don't use it already you really want to try it out)

    0 讨论(0)
  • 2020-11-28 05:39

    (this answer is in Kotlin, for Java see https://stackoverflow.com/a/64061936)

    For those using kotlin and jackson, here is the ValidatedList class that do not require wrapping, that is, it will still be serialized/deserialized as a usual list:

    class ValidatedList<E> {
        /**
         * By default, spring-boot cannot validate lists, as they are generic AND do not conform to the Java Bean definition.
         * This is one work-around: create a wrapper that fits the Java Bean definition, and use Jackson annotations to
         * make the wrapper disappear upon (de)serialization.
         * Do not change anything (such as making the _value field private) or it won't work anymore !
         * 
         * Usage:
         * ```
         * @PostMapping("/something")
         * fun someRestControllerMethod(@Valid @RequestBody pojoList: ValidatedList<SomePOJOClass>){
         *     // access list with:
         *     pojoList.values
         *}
         * ```
         */
    
        @JsonValue
        @Valid
        @NotNull
        @Size(min = 1, message = "array body must contain at least one item.")
        var _values: List<E>? = null
    
        val values: List<E>
            get() = _values!!
    
        @JsonCreator
        constructor(vararg list: E) {
            this._values = list.asList()
        }
    }
    

    Advantages:

    • no need for the @Validated annotation
    • will throw an error if the body is an empty array (see @Size)
    • the exception will be mapped correctly to 400 Bad Request (which is not the case when using javax and @Validated annotation)

    Example:

    data class N(
        @field:Min(value = 0, message = "id must be positive.")
        val id: Long? = null,
    
        @field:NotNull
        @field:Size(min = 2, max = 32, message = "wrong size: should be 32 chars long.")
        val token: String? = null
    )
    
    @RestController
    class XController {
        @PostMapping("/ns")
        fun getNs(@Valid @NotNull @RequestBody wrap: ListWrapper<N>) = wrap
    }
    

    Submit ok:

     curl -H "Content-Type: application/json" -X POST http://localhost:8080/ns -d '[{"id": 11, "token": "something"}]'
    
    [{"id" : 11, "token" : "something"}]
    

    Submit empty body:

    curl -H "Content-Type: application/json" -X POST http://localhost:8080/ns -d '[]'
    
    {
       "timestamp" : "2020-09-25T08:49:30.324+00:00",
       "message" : "Validation failed for object='listWrapper'. Error count: 1",
       "error" : "Bad Request",
       "path" : "/ns",
       "status" : 400,
       "exception" : "org.springframework.web.bind.MethodArgumentNotValidException",
       "trace":"org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public com.example.demo.test.XController$ListWrapper<com.example.demo.test.XController$N> com.example.demo.test.XController.getNs(com.example.demo.test.XController$ListWrapper<com.example.demo.test.XController$N>): [Field error in object 'listWrapper' on field '_values': rejected value [[]]; codes [Size.listWrapper._values,Size._values,Size.java.util.List,Size]; [...]"
    }
    

    Submit invalid items:

    curl -H "Content-Type: application/json" -X POST http://localhost:8080/ns -d '[{"id": -11, "token": ""}]'
    
    {
       "message" : "Validation failed for object='listWrapper'. Error count: 2",
       "path" : "/ns",
       "exception" : "org.springframework.web.bind.MethodArgumentNotValidException",
       "timestamp" : "2020-09-25T08:49:54.505+00:00",
       "error" : "Bad Request",
       "status" : 400,
       "trace":"org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public com.example.demo.test.XController$ListWrapper<com.example.demo.test.XController$N> com.example.demo.test.XController.getNs(com.example.demo.test.XController$ListWrapper<com.example.demo.test.XController$N>) with 2 errors: [...]"
    }
    
    0 讨论(0)
  • 2020-11-28 05:43

    Here's my attempt to reconcile the many different answers.

    Lebecca's answer works without the need for a wrapper, as Paul's answer requires, because @Validated placed on the class enables the method validation feature of the Bean Validation API.

    The Hibernate Validator documentation specifically explains:

    [...] the @Valid annotation can be used to mark executable parameters and return values for cascaded validation.

    [...]

    Cascaded validation can not only be applied to simple object references but also to collection-typed parameters and return values. This means when putting the @Valid annotation to a parameter or return value which

    • is an array

    • implements java.lang.Iterable

    • or implements java.util.Map

    each contained element gets validated.

    If you need to validate a collection of Beans, this is the most convenient way (make sure to also implement an @ExceptionHandler as required).

    If you need to validate a collection of Non-Beans, e.g. a List<String> where each element must match a pattern, you can use container element constraints like this:

    controllerMethod(List<@Pattern(regexp="pattern") String> strings)
    

    There's also the possibility to only use @Valid on a controller method parameter (which must then be a Bean type) without also placing @Validated on the class. In that case, you get an appropriate, detailed HTTP 400 response "for free", i.e. without the need for a custom @ExceptionHandler. But this doesn't apply the cascading validation, so you cannot validate something like @Valid List<SomeBean> beans, nor does it support container element constraints.

    And finally, you can combine the latter approach with an extra parameter added to the method of type BindingResult. This won't trigger an automatic error response in the case of a validation error, but instead you must inspect the injected BindingResult yourself in the method body and act accordingly (which allows for more flexibility). That is described in this comprehensive answer.

    0 讨论(0)
  • 2020-11-28 05:43

    create entity class:

    import javax.validation.Valid;
    import java.util.List;
    
    public class ValidList<E> {
    
        @Valid
        private List<E> list;
    
        public List<E> getList() {
            return list;
        }
    
        public void setList(List<E> list) {
            this.list = list;
        }
    }
    

    use Controller

        @RequestMapping(value = "/sku", method = RequestMethod.POST)
        public JsonResult createSKU(@Valid @RequestBody ValidList<Entity> entityList, BindingResult bindingResult) {
            if (bindingResult.hasErrors())
                return ErrorTools.build().handlerError(bindingResult);
            return new JsonResult(200, "result");
        }
    
    0 讨论(0)
提交回复
热议问题