86

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?

buræquete
  • 14,226
  • 4
  • 44
  • 89
TheZuck
  • 3,513
  • 2
  • 29
  • 36

21 Answers21

75

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 ...
}
Paul Strack
  • 783
  • 5
  • 2
  • 4
    This is the most elegant way I have ever found, thank you! – vr3C Jun 24 '16 at 05:48
  • 2
    This is really a crisp solution, but have a question - how to handle validation message in this scenario ? – Ritesh Kaushik Jul 21 '17 at 12:58
  • did you find elegant solution on how to handle message, I tried this but its throwing ConstraintViolationException . I want to handle using spring exception advice.(@ControllerAdvice) – user3444718 Jan 17 '18 at 15:35
  • this solution returns 415 Unsupported Media Type! Way? – Pascal May 09 '18 at 05:54
  • 7
    It's a working solution, but isn't spring violating JSR-303 which explicitly says Lists should be validated? `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. This includes specifically: • arrays of objects • java.util.Collection • java.util.Set • java.util.List • java.util.Map (special treatment see below) ` https://beanvalidation.org/1.0/spec/#constraintdeclarationvalidationprocess – gnomed Sep 10 '18 at 21:45
  • 6
    ValidList is supposed to @Override all of the List methods? Talking about boilerplate code... – Miguel Reyes Feb 18 '19 at 18:49
  • I'm getting "**415 Unsupported Media Type**" with following error: Failed to evaluate Jackson deserialization for type [[collection type; class com.ValidList, contains [simple type, class com.CompanyTag]]]: com.fasterxml.jackson.databind.JsonMappingException: Cannot find a deserializer for non-concrete Collection type [collection type; class com.ValidList, contains [simple type, class com.CompanyTag] – Nobita May 23 '19 at 13:44
  • Just use @Delegate with lombok, it will override all of the list methods for you – Xegara Oct 22 '21 at 10:24
62

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.

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.

Lebecca
  • 2,406
  • 15
  • 32
  • 3
    This will mean your rest controller itself will throw an exception rather than returning an appropriate 400 response though. – Sebastiaan van den Broek Mar 11 '19 at 11:25
  • This is an easy implementation fix and you can use it in conjunction with `@ControllerAdvice` and a GlobalExceptionHandler to catch the controller throwing the exception itself as Sebeastiann mentioned. Catching `ConstraintViolationException` allowed me to pretty up the return message to user and send back Status 400. – Encryption May 22 '19 at 15:58
  • @Encryption Wise suggestion, I use a HandlerExceptionResolver to handle it. – Lebecca May 23 '19 at 01:29
  • May work in Spring Boot 2 but doesn't work in Spring Boot 1.5.x – GameSalutes Jun 25 '19 at 04:30
  • @GameSalutes Sorry to hear that, I test with `Spring boot 1.5.0.RELEASE` and it works fine, but exceptions needed to be handled carefully. – Lebecca Jun 25 '19 at 13:12
  • This worked for me, required less boilerplate code but i rewrited the exception handler to cover my needs. – nastrand Sep 21 '19 at 15:10
  • Lucky me have been using `@ControllerAdvice` and class-scope `@Validated` already and was wondering why it needs to be so complicated. Thanks for the answer. – T Tse Jun 08 '20 at 09:22
  • For those wondering what the difference between this and Paul's answer is, this solution has @Validated at the Controller class level and will result in a ConstraintViolationException being raised while Paul's approach will result in MethodArgumentNotValidException being raised. – Sanjiv Jivan Jan 12 '22 at 09:30
29

@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)

laffuste
  • 16,287
  • 8
  • 84
  • 91
  • 5
    The Lombok addition is a nice touch and works great. Didn't even know about `@Delegate`. Simplest workaround answer for what I would call a Spring bug that still exists on the SpringBoot 1.5.x release track. – GameSalutes Jun 25 '19 at 04:39
  • I am always getting the following error: org.hibernate.AnnotationException: ValidList collection type not supported for property: MyClass.attribute Any ideas? – Alwin Aug 22 '21 at 17:39
  • Best answer. You rock. – etech Dec 09 '21 at 22:55
  • Also, if you want to validate `@NotEmpty` on the list, just put the `@NotEmpty` annotation on the inner `List list` property. – etech Dec 09 '21 at 23:22
20

Using Spring Boot 2.4.1:

  1. Add the @Validated annotation to the class

  2. Move the @Valid annotation inside the diamond operator:

    @RestController
    @Validated          // <-- This activates the Spring Validation AOP interceptor
    public class MyController {
    
      ...
          @RequestBody List<@Valid CompanyTag> categories
                           // ^^^ - NOTE: the @Valid annotation is inside <> brackets
    
epox
  • 9,236
  • 1
  • 55
  • 38
Marco Lackovic
  • 6,077
  • 7
  • 55
  • 56
  • 4
    this returns a 500 instead of expected 400 Http code – Sayantan Dec 15 '21 at 07:47
  • @Sayantan the default status code returned by this is `400`: if you are getting `500` then it must be because of some other issue in your code – Marco Lackovic Dec 21 '21 at 10:34
  • You need to be more specific as to what qualifies as an "issue" in code. I am quite certain about it as I tried multiple times as this would have been a quick fix. However, I had to eventually change to `ValidatedList` as depicted here https://stackoverflow.com/a/36313615/3950849 and then it returns a 400. I was using spring boot 2.4.5, you can try it out yourself if you would like. – Sayantan Jan 05 '22 at 06:08
  • Thanks, this helped me with wrapper types `@Valid List<@Size(max=50) String> myList` – Nathan Niesen Feb 17 '22 at 15:28
  • Any idea why I get `JSR-303 validated property 'items[19]' does not have a corresponding accessor for Spring data binding - check your DataBinder's configuration (bean property versus direct field access)` error when using this approach? – basarito Jul 21 '22 at 10:08
15

I would suggest to wrap your List categories into some DTO bean and validate it. Beside of working validation you will benefit from more flexible API.

@RequestMapping(value="/map/update", method=RequestMethod.POST, produces = "application/json; charset=utf-8")
@ResponseBody
public ResponseEntityWrapper updateMapTheme(
    HttpServletRequest request, 
    @RequestBody @Valid TagRequest tagRequest,
    HttpServletResponse response
    ) throws ResourceNotFoundException, AuthorizationException {
...
}

public static class TagRequest {
    @Valid
    List<CompanyTag> categories;    
    // Gettes setters
}
Babl
  • 7,446
  • 26
  • 37
  • 2
    That is what i did (with generics to make it, well, generic), but this is a workaround and I'm wondering if Spring, being a relatively active framework which is constantly developed and improved, has not solved this issue already in a way I'm not familiar with. – TheZuck Jan 26 '15 at 14:48
  • Basically this is not a Spring issue but a Bean Validation JSR implementation limitation. Which is in this case Hibernate Validator. And more over as the name for JSR tells us this is a "Bean Validation" not an "Object Validation" . You want to validate a List which is not a Java Bean. So I think the implementation accepts a bean object with getters and setters and then just validates the properties, but List is not the case. – Babl Jan 26 '15 at 15:46
  • 7
    Please note that you have to change the format of JSON from [{},{}] to {categories: [{},{}]} – Kacper86 Nov 30 '15 at 12:52
6

I think the most elegant solution is to create a custom Validator for Collection and a @ControllerAdvice that registers that Validator in the WebDataBinders, take a look to Spring validation for RequestBody parameters bound to collections in Controller methods

Community
  • 1
  • 1
eruiz
  • 1,963
  • 1
  • 14
  • 22
  • Can actually combine this method with the constraint validation by having the Validator invoke the bean validation manually following the strategy detailed here: https://farenda.com/java/bean-validation-unit-testing/ – GameSalutes Jun 25 '19 at 04:32
5

I did the below steps to make validation work on lists:

  1. Annotate the rest controller with @Validated at the class level
  2. Add @Valid before the generic type in the list, i.e List<@Valid MyClass>

Also, found that if the validation failed I got javax.validation.ConstraintViolationException

Laurel
  • 5,965
  • 14
  • 31
  • 57
ayman.mostafa
  • 451
  • 6
  • 5
4

Validating a collection does not work directly.

For example: what should it do if multiple elements fail the validation? Stop after first validation? Validate all (if so what is to be done with the collection of messages)?

If in your configuration Spring delegates to a Bean Validator provider like Hibernate Validator, you should look up for ways of implementing a collection validator there.

For Hibernate, a similar problem is discussed here

Community
  • 1
  • 1
Cristian Sevescu
  • 1,364
  • 8
  • 11
4

use @Validated annotate controller
use @Valid annotate @RequestBody

MattonRoi
  • 109
  • 1
  • 5
4

The @Valid annotation can be used inside the diamond operator:

private List<@Valid MyType> types;

or

@Valid
private List<MyType> types;

Now, every list item will be validated.

Hamid Mohayeji
  • 3,977
  • 3
  • 43
  • 55
  • Question is about collection parameter in controller method. Tl;DR doesn't work there. – zeratul021 Mar 12 '20 at 23:14
  • 1
    @Hamid's first example is perfect when validating a value based on an enum. Note that the validator should include TYPE_USER in the @Target({ TYPE, FIELD, ANNOTATION_TYPE, PARAMETER, TYPE_USE }) annotation to support this type of in-line validator – Dana Feb 24 '21 at 19:18
  • this still won't work until you have `@Validated` on the controller class itself and additionally, this returns a 500 and not the expected 400 Http Code – Sayantan Dec 15 '21 at 07:59
4

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.

Hein Blöd
  • 1,553
  • 1
  • 18
  • 25
4

With the later versions of spring, you can now do this.

@RequestMapping(value="/map/update", method=RequestMethod.POST, produces = "application/json; charset=utf-8")
@ResponseBody
public ResponseEntityWrapper updateMapTheme(
        HttpServletRequest request, 
        @RequestBody List<@Valid CompanyTag> categories,
        HttpServletResponse response
        ) throws ResourceNotFoundException, AuthorizationException {
...
}

the @Valid annotation is in the generic param.

If you are using a custom javax validation annotation, make sure to add TYPE_USE to the annotation targe

@Target({ ElementType.TYPE_USE})
public @interface ValidationAnnotation {.. }
Borislav Stoilov
  • 3,247
  • 2
  • 21
  • 46
3

I'm using spring-boot 1.5.19.RELEASE

I annotate my service with @validated and then apply @Valid to the List parameter in the method and items in my list get validated.

Model

@Data
@ApiModel
@Validated
public class SubscriptionRequest {
    @NotBlank()
    private String soldToBpn;

    @NotNull
    @Size(min = 1)
    @Valid
    private ArrayList<DataProducts> dataProducts;

    private String country;

    @NotNull
    @Size(min = 1)
    @Valid
    private ArrayList<Contact> contacts;
}

Service Interface (or use on concrete type if no interface)

@Validated
public interface SubscriptionService {
    List<SubscriptionCreateResult> addSubscriptions(@NonNull @Size(min = 1) @Valid List<SubscriptionRequest> subscriptionRequestList)
        throws IOException;
}

Global Exception Handler method (ApiError Type is not my design)

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(value = ConstraintViolationException.class)
@ResponseBody
public ApiError[] handleConstraintViolationException(ConstraintViolationException exception) {
    List<InvalidField> invalidFields = exception.getConstraintViolations().stream()
        .map(constraintViolation -> new InvalidField(constraintViolation.getPropertyPath().toString(),
                                                     constraintViolation.getMessage(),
                                                     constraintViolation.getInvalidValue()))
        .collect(Collectors.toList());
    return new ApiError[] {new ApiError(ErrorCodes.INVALID_PARAMETER, "Validation Error", invalidFields)};
}

example bad method call from a controller

 LinkedList<SubscriptionRequest> list = new LinkedList<>();
 list.add(new SubscriptionRequest());
 return subscriptionService.addSubscriptions(list);

Response body (note the index [0])

[
    {
        "errorCode": "invalid.parameter",
        "errorMessage": "Validation Error",
        "invalidFields": [
            {
                "name": "addSubscriptions.arg0[0].soldToBpn",
                "message": "may not be empty",
                "value": null
            },
            {
                "name": "addSubscriptions.arg0[0].dataProducts",
                "message": "may not be null",
                "value": null
            },
            {
                "name": "addSubscriptions.arg0[0].contacts",
                "message": "may not be null",
                "value": null
            }
        ]
    }
]
Richard Collette
  • 5,462
  • 4
  • 53
  • 79
1

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");
    }
QiongLee
  • 21
  • 2
1

For those using spring boot (I was using 2.6.7), what worked for me was adding the spring-boot-starter-validation dependency:

org.springframework.boot:spring-boot-starter-validation
Ian S.
  • 1,831
  • 9
  • 17
agulan
  • 11
  • 3
1

I am using

  • Kotlin 1.6
  • Spring Boot 2.6.6
  • Spring Webflux

I needed to validate a List<String> request parameters. Here is my working example (inspired by some of previous answers)

@RestController
@Validated
class SearchController {
    @GetMapping("/search")
    fun search(
        @Valid
        @RequestParam(value = "term") terms: List<Term>,
    ): Mono<ResponseEntity<SearchResponse>> {...}
}

data class Term(
    @field:NotEmpty(
        message = "Term is required"
    )
    @field:Size(
        min = 2,
        max = 500,
        message = "Term length out of range"
    )
    val term: String
)

in build.gradle.kts

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-validation")
}
Olga
  • 91
  • 4
0

(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: [...]"
}
Derlin
  • 9,572
  • 2
  • 32
  • 53
0

With the Spring Boot 2.2.2 version...

Here's the piece of code:-

import java.util.List;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Validated
public class MyController {
    
    @PostMapping(value = "/test", consumes = "application/json", produces = "application/json")
    public String test(@Valid @RequestBody List<Student> st) {
        System.out.println("-------------test Method-------");
        return "Its' Success";
    }
}

class Student{
    
    @NotBlank
    String name;
    @NotBlank
    String password;
    @NotBlank
    String email;
    
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getPassword() {
        return password;
    }
    public void setPassword(String password) {
        this.password = password;
    }
    public String getEmail() {
        return email;
    }
    public void setEmail(String email) {
        this.email = email;
    }
}

List of JSON Data:-

Notice name is blank in the second Student object.

[
  {
        "name": "Sreepad",
        "password": "sddwh",
        "email": "sample@gmail.oom"
    },
    {
        "name": "",
        "password": "sddwh",
        "email": "sample@gmail.oom"
    }
]

Error Description:-

javax.validation.ConstraintViolationException: test.st[1].name: must not be blank.

Note: List and String won't be validated at method parameter level if you remove @Validated at Class level.

SpringBoot doc says:-

17. Validation

The method validation feature supported by Bean Validation 1.1 is automatically enabled as long as a JSR-303 implementation (such as Hibernate validator) is on the classpath. This lets bean methods be annotated with javax.validation constraints on their parameters and/or on their return value. Target classes with such annotated methods need to be annotated with the @Validated annotation at the type level for their methods to be searched for inline constraint annotations.

Sreepad Chitragar
  • 183
  • 1
  • 3
  • 12
0

I have done custom Validation for the list of parameters we're passing... `

    import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import javax.validation.Constraint;
import javax.validation.Payload;

@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = PatternListValidator.class)
public @interface PatternList {

    public String regexp();
    public String message() default "Invalid inputs";
    public Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}`

Created the above custom validation annotation / interface and implemented the same with the business logic


    import java.util.List;
    
    import javax.validation.ConstraintValidator;
    import javax.validation.ConstraintValidatorContext;
    
    public class PatternListValidator implements ConstraintValidator<PatternList, List<String>> {
    
        private String regexp;
    
        @Override
        public void initialize(PatternList patternList) {
            this.regexp = patternList.regexp();
        }
    
        @Override   
        public boolean isValid(List<String> dataList, ConstraintValidatorContext context) {
    
            for(String data : dataList) {
                if(!data.matches(regexp)) {
                    return false;
                }
            }
            return true;
        }
    
    }

used this @PatternList annotation in my controller class as api method parameter as below

 

        Public ResponseEntity<Object> getStudents(
    @ApiParam(name = "studentIds", value = "Fetch students for athlete and art. Example values: 1234, 5432", required = true) @PatternList(regexp = "\\d+", message = "student Id's can contain only numbers") @RequestParam(value = "studentId", required = true) List<String> studentIds) {
            
    business logic goes here....
    
    }
0

To add to the above by @laffuste with Lombok, in Spring Boot 2.7, I have a MyDtoList validator that delegates back down to the singular validator for a plural argument. In my Spring RestController that has singular and plural arguments:

In application yaml:

spring:
  jackson:
    deserialization:
      accept-single-value-as-array: true

In my controller:

@InitBinder("myDto")
public void addMyDtoValidator(WebDataBinder binder) {
    binder.addValidators(myDtoValidator);
}

@InitBinder("myDtoList")
public void addMyDtoListValidator(WebDataBinder binder) {
    binder.addValidators(myDtoListValidator);
}

Then the validator code:

  private MyDtoValidator singleDtoValidator;

  public MyDtoListValidator(MyDtoValidator singleDtoValidator) {
    this.singleDtoValidator = singleDtoValidator;
  }

          
  @Override
  public boolean supports(Class<?> clazz) {
    return ValidList.class.isAssignableFrom(clazz);
  }

  @Override
  public void validate(Object target, Errors errors) {
    if (target == null) {
      errors.rejectValue(null, "should not be null error");
    } else {
      ValidList<MyDto> list = (ValidList<MyDto>) target;
      for (MyDtodto: list) {
        singleDtoValidator.validate(dto, errors);
      }
    }
  }
Randy
  • 729
  • 5
  • 14
0

After several tries, I figured it out this solution! Hope will be useful for you.

Verify! are you pointing to Java 17 from build path (Java 17 or higher, as Spring Boot 3.x is used which brings Hibernate-Validator 8.0.0.Final)

@PostMapping
@ResponseStatus(code = HttpStatus.OK)
public List<CompanyTag> updateMapTheme(@RequestBody List<@Valid CompanyTag> companyTag) {
       return appService.updateMapTheme(companyTag);
}

Make sure that @Valid should be inside diamond brackets! @RequestBody List<@Valid Event> events

Finally! Restart the IDE once you change the JDK Path to 17 or higher.

Cheers! Happy coding :)

Prashant
  • 616
  • 6
  • 10