Spring MVC - @Valid on list of beans in REST service
Asked Answered
F

10

41

In a Spring MVC REST service (json), I have a controller method like this one :

@RequestMapping(method = RequestMethod.POST, value = { "/doesntmatter" })
@ResponseBody
public List<...> myMethod(@Valid @RequestBody List<MyBean> request, BindingResult bindingResult) {

Where the MyBean class has bean validation annotations.

The validations don't seem to take place in this case, although it works well for other controllers.

I don't want to encapsulate the list in a dto this that would change the json input.

Why is there no validations for a list of beans ? What are the alternatives ?


Forecastle answered 20/6, 2013 at 7:32 Comment(1)
you can use this https://mcmap.net/q/138380/-validation-of-a-list-of-objects-in-springIsom
T
57

@Valid is a JSR-303 annotation and JSR-303 applies to validation on JavaBeans. A java.util.List is not a JavaBean (according to the official description of a JavaBean), hence it cannot be validated directly using a JSR-303 compliant validator. This is supported by two observations.

Section 3.1.3 of the JSR-303 Specification says that:

In addition to supporting instance validation, validation of graphs of object is also supported. The result of a graph validation is returned as a unified set of constraint violations. Consider the situation where bean X contains a field of type Y. By annotating field Y with the @Valid annotation, the Validator will validate Y (and its properties) when X is validated. The exact type Z of the value contained in the field declared of type Y (subclass, implementation) is determined at runtime. The constraint definitions of Z are used. This ensures proper polymorphic behavior for associations marked @Valid.

Collection-valued, array-valued and generally Iterable fields and properties may also be decorated with the @Valid annotation. This causes the contents of the iterator to be validated. Any object implementing java.lang.Iterable is supported.

I have marked the important pieces of information in bold. This section implies that in order for a collection type to be validated, it must be encapsulated inside a bean (implied by Consider the situation where bean X contains a field of type Y); and further that collections cannot be validated directly (implied by Collection-valued, array-valued and generally Iterable fields and properties may also be decorated, with emphasis on fields and properties).

Actual JSR-303 implementations

I have a sample application that tests collection validation with both Hibernate Validator and Apache Beans Validator. If you run tests on this sample as mvn clean test -Phibernate (with Hibernate Validator) and mvn clean test -Papache (for Beans Validator), both refuse to validate collections directly, which seems to be in line with the specification. Since Hibernate Validator is the reference implementation for JSR-303, this sample is further proof that collections need to be encapsulated in a bean in order to be validated.


With that cleared, I would say that there is also a design problem in trying to pass a collection to a controller method directly in the way shown in the question. Even if validations were to work on the collections directly, the controller method will be unable to work with alternate data representations such as custom XML, SOAP, ATOM, EDI, Google Protocol Buffers etc. which do not map directly to collections. For supporting those representations, the controller must accept and return object instances. That would require encapsulating the collection inside an object instance any way. It would therefore be highly advisable to wrap the List inside another object as other answers have suggested.

Tolbutamide answered 26/2, 2016 at 4:53 Comment(0)
L
13

The only way i could find to do this is to wrap the list, this also means that the JSON input would have to change.

@RequestMapping(method = RequestMethod.POST, value = { "/doesntmatter" })
@ResponseBody
public List<...> myMethod(@Valid @RequestBody List<MyBean> request, BindingResult bindingResult) {

becomes:

@RequestMapping(method = RequestMethod.POST, value = { "/doesntmatter" })
@ResponseBody
public List<...> myMethod(@Valid @RequestBody MyBeanList request, BindingResult bindingResult) {

and we also need:

import javax.validation.Valid;
import java.util.List;

public class MyBeanList {

    @Valid
    List<MyBean> list;

    //getters and setters....
}

This looks like it could also be possible with a custom validatior for lists but i have not got that far yet.

The @Valid annotation is part of the standard JSR-303 Bean Validation API, and is not a Spring-specific construct. Spring MVC will validate a @Valid object after binding so-long as an appropriate Validator has been configured.

Reference : http://docs.spring.io/spring/docs/current/spring-framework-reference/html/validation.html

Lyric answered 23/2, 2016 at 21:50 Comment(0)
A
5

Try direct validation. Something like this:

@Autowired
Validator validator;

@RequestMapping(method = RequestMethod.POST, value = { "/doesntmatter" })
@ResponseBody
public Object myMethod(@RequestBody List<Object> request, BindingResult bindingResult) {
    for (int i = 0; i < request.size(); i++) {
        Object o = request.get(i);
        BeanPropertyBindingResult errors = new BeanPropertyBindingResult(o, String.format("o[%d]", i));
        validator.validate(o, errors);
        if (errors.hasErrors())
            bindingResult.addAllErrors(errors);
    }
    if (bindingResult.hasErrors())
        ...
Ardyce answered 24/2, 2016 at 21:40 Comment(1)
Yes, but it's a lot of lines that would be avoided with an annotation.Kim
S
4

There is a elegant way to wrapper your request in a custom java.util.List which acts as both List and JavaBean. see here

Susa answered 27/6, 2016 at 7:26 Comment(0)
C
4

Using com.google.common.collect.ForwardingList

public class ValidList<T> extends ForwardingList<T> {

  private List<@Valid T> list;

  public ValidList() {
    this(new ArrayList<>());
  }

  public ValidList(List<@Valid T> list) {
    this.list = list;
  }

  @Override
  protected List<T> delegate() {
    return list;
  }

  /** Exposed for the {@link javax.validation.Validator} to access the list path */
  public List<T> getList() {
    return list;
  }
}

So no need for the wrapper

you may use

@RequestMapping(method = RequestMethod.POST, value = { "/doesntmatter" })
@ResponseBody
public List<...> myMethod(@Valid @RequestBody ValidList<MyBean> request, BindingResult bindingResult) {

By using wrapper your JSON needs to be changed to

{
  "list": []
}

with this implementation you can use original JSON

[]
Corneliacornelian answered 9/5, 2019 at 10:58 Comment(0)
M
4

Given Spring-Boot + Jackson for JSON serialization + org.springframework.boot:spring-boot-starter-validation (must be included manually for spring boot >= 2.3.0)

Using built-ins

  • add @Validated to your controller
  • use @Valid @NotNull @RequestBody List<@Valid Pojo> pojoList in your controller method signature

This will throw a javax.validation.ConstraintViolationException error on invalid beans though, which is mapped to 500 Internal Error by default. Hence, ensure you have a ControllerAdvice for this as well !

Using a wrapper

A list wrapper is nice (that is, a class with a single field of type List<E>), but from the responses above you will have to change the JSON as well ({"list": []} vs []), which is not nice...

Solution:

  • in the wrapper, use @JsonValue annotation on the wrapped list field
  • add a constructor taking a list as argument, and annotate it with @JsonCreator
  • in your controller method, use @Valid @RequestBody ListWrapper<Pojo> tokenBodies

This works, is elegant, and doesn't require anything more. Moreover, it will throw the usual org.springframework.web.bind.MethodArgumentNotValidException on invalid beans.


Wrapper Example (java):

(For a full example in Kotlin, see https://stackoverflow.com/a/64060909)

public class ValidList<E> {
    @JsonValue
    @Valid
    @NotNull
    @Size(min = 1, message = "array body must contain at least one item.")
    private List<E> values;

    @JsonCreator
    public ValidList(E... items) {
        this.values = Arrays.asList(items);
    }

    public List<E> getValues() {
        return values;
    }

    public void setValues(List<E> values) {
        this.values = values;
    }
}
public class SomePojo {
    @Min(value = 1)
    int id;

    @Size(min = 2, max = 32)
    String token;

    // getters and setters
}
@RestController
public class SomeController {

    @PostMapping("/pojos")
    public ValidList<SomePojo> test(@Valid @RequestBody ValidList<SomePojo> pojos) {
        return pojos;
    }
}

Submit ok:

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

Submit empty body:

curl -H "Content-Type: application/json" -X POST http://localhost:8080/ns -d '[]'
{
   "timestamp" : "2020-09-25T09:55:05.462+00:00",
   "error" : "Bad Request",
   "message" : "Validation failed for object='validList'. Error count: 1",
   "exception" : "org.springframework.web.bind.MethodArgumentNotValidException",
   "path" : "/pojos",
   "status" : 400,
   "trace": "org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public com.example.demo.ValidList<com.example.demo.SomePojo> com.example.demo.SomeController.test(com.example.demo.ValidList<com.example.demo.SomePojo>): [Field error in object 'validList' on field 'values': rejected value [[]]; codes [Size.validList.values,Size.values,Size. [...]"
}

Submit invalid items:

curl -H "Content-Type: application/json" -X POST http://localhost:8080/ns -d '[{"id": -11, "token": ""}]'
{
   "timestamp" : "2020-09-25T09:53:56.226+00:00",
   "error" : "Bad Request",
   "message" : "Validation failed for object='validList'. Error count: 2",
   "exception" : "org.springframework.web.bind.MethodArgumentNotValidException",
   "path" : "/pojos",
   "status" : 400,
   "trace": "org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public com.example.demo.ValidList<com.example.demo.SomePojo> com.example.demo.SomeController.test(com.example.demo.ValidList<com.example.demo.SomePojo>) with 2 errors: [Field error in object 'validList' on field 'values[0].id': rejected value [-11]; co [...]"
}
Merrick answered 25/9, 2020 at 9:59 Comment(0)
G
2

Implement your own validator with org.springframework.validation.beanvalidation.LocalValidatorFactoryBean as a member and call that validator for each item.

public class CheckOutValidator implements Validator {


    private Validator validator;

   @Override
    public void validate(Object target, Errors errors) { 
    List request = (List) target;
    Iterator it = request.iterator()   
    while(it.hasNext()) {
    MyBean b = it.next();
    validator.validate(b, errors);

     }

     }

//setters and getters

}
Gragg answered 21/6, 2013 at 6:47 Comment(0)
A
1

If you don't want to write a wrapper for each List you have, you can use a generic wrapper:

public class ListWrapper<E> {

    private List<E> list;

    public ListWrapper() {
        list = new ArrayList<>();
    }

    public ListWrapper(List<E> list) {
        this.list = list;
    }

    @Valid
    public List<E> getList() {
        return list;
    }

    public void setList(List<E> list) {
        this.list = list;
    }

    public boolean add(E e) {
        return list.add(e);
    }

    public void clear() {
        list.clear();
    }

}
Archle answered 21/6, 2017 at 7:48 Comment(0)
M
0

I think your best option is to wrap the list - How to validate request parameter if it is not a bean in spring MVC?

There is no way atm to say that the @Valid applies to the elements of the collection.

Manyplies answered 21/6, 2013 at 9:4 Comment(1)
Is there any way to wrap the list in a way that that would not change the json input?Lyric
B
-4
@Valid @RequestBody List<MyBean> request

works for me so long as you submitting valid json:-

[
    {
        "property1": "value1",
        "property2": "value2"
      },
    {
        "property1": "value3",
        "property2": "value4"
        }
]
Beane answered 14/5, 2019 at 10:41 Comment(1)
This indeed works if you submit valid JSON, but the question was about how to trigger the validation with @Valid if the JSON is not valid...Mahican

© 2022 - 2024 — McMap. All rights reserved.