web-dev-qa-db-fra.com

Validation d'une liste d'objets au printemps

J'ai la méthode de contrôleur suivante:

@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 est défini de cette façon:

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

Le problème est que la validation n'est pas déclenchée, la liste CompanyTag n'est pas validée, le validateur "StringUUIDValidation" n'est jamais appelé.

Si je supprime la liste et que j'essaie d'envoyer un seul CompanyTag, c'est-à-dire au lieu de:

@RequestBody @Valid List<CompanyTag> categories,

utilisation:

@RequestBody @Valid CompanyTag category,

cela fonctionne comme prévu, donc apparemment, Spring n'aime pas valider les listes de choses (essayé avec un tableau à la place, cela n'a pas fonctionné non plus).

Quelqu'un a une idée de ce qui manque?

38
TheZuck

J'ai trouvé une autre approche qui fonctionne. Le problème de base est que vous voulez avoir une liste comme charge utile d'entrée pour votre service, mais javax.validation ne validera pas une liste, seulement un JavaBean. L'astuce consiste à utiliser une classe de liste personnalisée qui fonctionne à la fois comme un List et un JavaBean:

@RequestBody @Valid List<CompanyTag> categories

Changer pour:

@RequestBody @Valid ValidList<CompanyTag> categories

Votre sous-classe de liste ressemblerait à ceci:

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 ...
}
47
Paul Strack

Réponse d'origine

J'ai essayé d'utiliser la méthode de Paul dans mon projet, mais certaines personnes ont dit que c'était trop complexe. Peu de temps après cela, je trouve un autre moyen simple qui fonctionne comme le code ci-dessous:

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

  }
}

Cela fonctionne et facile à utiliser. Le point clé est l'annotation @Valiated sur la classe. Btw, c'est springBootVersion = '2.0.4.RELEASE' que j'utilise.

mise à jour pour la partie poignée d'exception

Comme indiqué dans les commentaires, nous devons gérer les exceptions ici comme le code ci-dessous:

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

Prendre le code d'état http comme représentant le réseau est correct et seul le premier message de violation est renvoyé ici. Vous pouvez le modifier pour satisfaire des exigences personnalisées.

11
Lebecca

Je suggère d'envelopper vos catégories de listes dans un bean DTO et de le valider. En plus de la validation de travail, vous bénéficierez d'une API plus flexible.

@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
}
9
Babl

Je pense que la solution la plus élégante consiste à créer un Validator for Collection personnalisé et un @ControllerAdvice qui enregistre ce Validator dans les WebDataBinders, jetez un œil à Validation Spring pour les paramètres RequestBody liés aux collections dans les méthodes Controller

5
eruiz

@Paul Strack's excellente solution mélangé avec la magie de Lombok:

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

Utilisation (permuter la liste pour ValidList):

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

(Besoin Lombok , mais si vous ne l'utilisez pas déjà, vous voulez vraiment l'essayer)

4
laffuste

La validation d'une collection ne fonctionne pas directement.

Par exemple: que faire si plusieurs éléments échouent à la validation? Arrêter après la première validation? Valider tout (si oui, que faire de la collecte des messages)?

Si, dans votre configuration, Spring délègue à un fournisseur de validateur de bean comme Hibernate Validator, vous devriez rechercher des moyens d'implémenter un validateur de collection.

Pour Hibernate, un problème similaire est discuté ici

3
Cristian Sevescu

utiliser @ Validated annoter le contrôleur
utilisez @ Valid annotez @RequestBody

1
ShaneSun

J'utilise Spring-Boot 1.5.19.RELEASE

J'annote mon service avec @validated puis appliquez @Valid au paramètre List dans la méthode et les éléments de ma liste sont validés.

Modèle

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

Interface de service (ou utilisation sur type concret si aucune interface)

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

Méthode Global Exception Handler (le type ApiError n'est pas ma conception)

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

exemple d'appel de mauvaise méthode à partir d'un contrôleur

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

Corps de réponse (notez l'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
            }
        ]
    }
]
1
Richard Collette

créer une classe d'entité:

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

utiliser le contrôleur

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