Проверка списка объектов в Spring

У меня есть следующий метод контроллера:

@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 определяется следующим образом:

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;
    }
   ...
}

Проблема в том, что проверка не инициируется, список CompanyTag не проверяется, валидатор "StringUUIDValidation" никогда не вызывается.

Если я удаляю Список и пытаюсь отправить только один CompanyTag, т.е. вместо:

@RequestBody @Valid List<CompanyTag> categories,

использовать:

@RequestBody @Valid CompanyTag category,

он работает, как и ожидалось, поэтому, видимо, Spring не любит проверять списки вещей (пробовал с массивом, который тоже не работал).

У кого-нибудь есть идеи, чего не хватает?

21 ответ

Я нашел другой подход, который работает. Основная проблема заключается в том, что вы хотите иметь список в качестве входных данных для своего сервиса, но javax.validation не будет проверять список, только JavaBean. Хитрость заключается в том, чтобы использовать пользовательский класс списка, который функционирует как List и JavaBean:

@RequestBody @Valid List<CompanyTag> categories

Изменить на:

@RequestBody @Valid ValidList<CompanyTag> categories

Ваш подкласс списка будет выглядеть примерно так:

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 ...
}

Я пытался использовать метод Пола в своем проекте, но некоторые люди говорили, что он слишком сложный. Вскоре после этого я нахожу другой простой способ, который работает как код ниже:

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

  }
}

Работает и прост в использовании. Ключевым моментом является аннотация @Valiated для класса. Кстати, это SpringBootVersion = '2.0.4.RELEASE', который я использую.

Великолепное решение @Paul Strack'а, смешанное с магией Ломбок:

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

Использование (список подкачки для ValidList):

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

(Нуждается в Lombok, но если вы уже не используете его, вы действительно хотите попробовать его)

Использование Spring Boot 2.4.1:

  1. Добавить @Validated аннотация к классу

  2. Переместите @Valid аннотация внутри оператора ромба:

            @RequestBody List<@Valid CompanyTag> categories
    

Я бы предложил обернуть ваши категории List в некоторый компонент DTO и проверить его. Помимо рабочей проверки вы получите более гибкий 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
}

Я сделал следующие шаги, чтобы проверка работала в списке:
1-Аннотировать остальной контроллер с помощью @Valiodated на уровне класса
2-Добавить @Valid перед общим типом в списке, т.е. List<@Vlaid MyClas>
Кроме того, обнаружил, что если проверка не удалась, я получил javax.validation.ConstraintViolationException

Я думаю, что наиболее элегантным решением является создание пользовательского Validator для Collection и @ControllerAdvice, который регистрирует этот Validator в WebDataBinders, взгляните на проверку Spring для параметров RequestBody, связанных с коллекциями в методах Controller.

С более поздними версиями Spring теперь вы можете это сделать.

      @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 {
...
}

аннотация @Valid находится в общем параметре. Однако, если вы хотите проверить весь список, вам все равно нужно обернуть его в объект.

Использовать @Validated аннотировать контроллер
используйте @Valid annotate @RequestBody

В @Valid аннотацию можно использовать внутри оператора ромба:

private List<@Valid MyType> types;

или

@Valid
private List<MyType> types;

Теперь каждый элемент списка будет проверен.

Вот моя попытка согласовать множество разных ответов.

Ответ Лебекки работает без необходимости в оболочке, как того требует ответ Пола, потому что@Validatedпомещенный в класс, включает функцию проверки методов API проверки компонентов.

Документация Hibernate Validator конкретно объясняет:

[...] аннотацию @Valid можно использовать для пометки исполняемых параметров и возвращаемых значений для каскадной проверки.

[...]

Каскадная проверка может применяться не только к простым ссылкам на объекты, но и к параметрам типа коллекции и возвращаемым значениям. Это означает, что при добавлении аннотации @Valid к параметру или возвращаемому значению

  • это массив

  • реализует java.lang.Iterable

  • или реализует java.util.Map

проверяется каждый содержащийся элемент.

Если вам нужно проверить коллекцию Beans, это наиболее удобный способ (не забудьте также реализовать@ExceptionHandler как требуется).

Если вам нужно проверить коллекцию не-Beans, напримерList<String>где каждый элемент должен соответствовать шаблону, вы можете использовать такие ограничения контейнерных элементов:

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

Также есть возможность использовать только@Validв параметре метода контроллера (который в таком случае должен быть типом Bean) без размещения@Validatedна классе. В этом случае вы получите соответствующий подробный ответ HTTP 400 "бесплатно", т. Е. Без необходимости настраивать@ExceptionHandler. Но при этом не применяется каскадная проверка, поэтому вы не можете проверить что-то вроде@Valid List<SomeBean> beans, а также не поддерживает ограничения элементов контейнера.

И, наконец, вы можете объединить последний подход с дополнительным параметром, добавленным к методу типа BindingResult. Это не вызовет автоматический ответ об ошибке в случае ошибки проверки, но вместо этого вы должны проверить введенныйBindingResultсебя в теле метода и действуйте соответственно (что обеспечивает большую гибкость). Это описано в этом исчерпывающем ответе.

Проверка коллекции не работает напрямую.

Например: что делать, если несколько элементов не прошли проверку? Остановить после первой проверки? Подтвердить все (если так, что делать с коллекцией сообщений)?

Если в вашей конфигурации Spring делегирует провайдера Bean Validator, такого как Hibernate Validator, вам следует поискать способы реализации там валидатора коллекции.

Для Hibernate аналогичная проблема обсуждается здесь

Я использую Spring-Boot 1.5.19.RELEASE

Я комментирую свой сервис с @validated а затем применить @Valid к List параметр в методе и элементы в моем списке проверяются.

модель

@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;
}

Сервисный интерфейс (или использовать на конкретном типе, если нет интерфейса)

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

Метод Global Exception Handler (ApiError Type не мой дизайн)

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

пример плохого вызова метода из контроллера

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

Тело ответа (обратите внимание на индекс [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
            }
        ]
    }
]

Создать класс сущности:

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;
    }
}

использовать контроллер

    @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");
    }

Для тех, кто использует весеннюю загрузку (я использовал 2.6.7), мне помогло добавление зависимости spring-boot-starter-validation:

      org.springframework.boot:spring-boot-starter-validation

После нескольких попыток я нашел это решение! Надеюсь, вам будет полезно.

Проверять! вы указываете на Java 17 из пути сборки (Java 17 или выше, поскольку используется Spring Boot 3.x , который обеспечивает Hibernate-Validator 8.0.0.Final)

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

Убедитесь, что @Valid находится внутри ромбовидных скобок!@RequestBody List<@Valid Event> events

Окончательно! Перезапустите IDE, как только вы измените путь JDK на 17 или выше.

Ваше здоровье! Приятного кодирования :)

Я использую

  • Котлин 1.6
  • Весенняя загрузка 2.6.6
  • Весенний вебфлюкс

Мне нужно было подтвердитьList<String>параметры запроса. Вот мой рабочий пример (вдохновленный некоторыми из предыдущих ответов)

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

в build.gradle.kts

      dependencies {
    implementation("org.springframework.boot:spring-boot-starter-validation")
}

(этот ответ находится в Котлине, для Java см. /questions/40099525/spring-mvc-valid-v-spiske-binov-v-rest-servise/56563176#56563176)

Для тех, кто использует Котлин и Джексон, вот ValidatedListкласс, который не требует обертывания, то есть он все равно будет сериализован / десериализован как обычный список:

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()
    }
}

Преимущества:

  • нет необходимости в @Validated аннотация
  • выдаст ошибку, если тело представляет собой пустой массив (см. @Size)
  • исключение будет правильно сопоставлено 400 Bad Request (чего не происходит при использовании javax и @Validated аннотация)

Пример:

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
}

Отправить ОК:

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

Отправить пустое тело:

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]; [...]"
}

Отправьте недействительные элементы:

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: [...]"
}

В версии Spring Boot 2.2.2 ...

Вот фрагмент кода:-

      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;
    }
}

Список данных JSON:-

Имя уведомления пусто во втором объекте Student.

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

Описание ошибки:-

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

Примечание. Список и строка не будут проверяться на уровне параметров метода, если вы удалите @Validated на уровне класса.

Документ SpringBoot говорит :-

17. Проверка

Функция проверки метода, поддерживаемая Bean Validation 1.1, автоматически включается, пока реализация JSR-303 (например, валидатор Hibernate) находится в пути к классам. Это позволяет аннотировать методы компонента с помощью ограничений javax.validation на их параметры и / или на их возвращаемое значение. Целевые классы с такими аннотированными методами необходимо аннотировать аннотацией @Validated на уровне типа, чтобы их методы искали аннотации встроенных ограничений.

Я выполнил пользовательскую проверку списка параметров, которые мы передаем... `

          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....
    
    }

Чтобы добавить к вышесказанному @laffuste с Lombok, в Spring Boot 2.7 у меня есть валидатор MyDtoList, который делегирует обратно валидатору в единственном числе для аргумента во множественном числе. В моем Spring RestController, который имеет аргументы в единственном и множественном числе:

В приложении yaml:

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

В моем контроллере:

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

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

Затем код валидатора:

        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);
      }
    }
  }
Другие вопросы по тегам