[踩雷紀錄] Spring boot validation 無法驗證 List、Set
2021, Oct 21
API 參數傳入 List 或是 Set 的例子也是屢見不鮮,但卻發現一般的 Validation 作法沒有辦法驗證到 List 的內容,不確定是不是 Bug,但還是必須要想點辦法來解
沒看過前一篇的可以點這邊:
情境
一樣按照前幾篇的例子來看,照以下寫法期望可以去驗證 List 中每個 UserDto 內的資料
@RestController()
@RequestMapping("/v1/user")
public class UserController {
@PostMapping("")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void createUsers(@RequestBody @Valid List<UserDto> dto) {
}
}
但實際上卻一個驗證都不會執行,如果要讓他生效需要一些其他的做法,大致查到有兩種解法
解法 1
在 Controller 的 class 層級加上 @Validated
,簡單加上一個 annotation 就可以讓驗證生效,只是會改變拋出的 Exception,記得將例外處理事先寫好,如果不知道怎麼寫的可以參考這裡
@Validated
@RestController()
@RequestMapping("/v1/user")
public class UserController {
@PostMapping("")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void createUsers(@RequestBody @Valid List<UserDto> dto) {
}
}
分組驗證
不過這個寫法直覺上如果想套用分組驗證的時候,會想跟一般寫法一樣,直接寫在參數裡,但這樣卻是不 work 的
@Validated
@RestController()
@RequestMapping("/v1/user")
public class UserController {
@PostMapping("")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void createUsers(@RequestBody @Validated(UserDto.Update.class) List<UserDto> dto) {
}
}
必須改為以下寫法,把 annotation 提到 method 層級才會生效,並且參數的 @Valid
也不能忘記加上,以上三個 annotation 缺一不可
@Validated
@RestController()
@RequestMapping("/v1/user")
public class UserController {
@PostMapping("")
@ResponseStatus(HttpStatus.NO_CONTENT)
@Validated(UserDto.Update.class)
public void createUsers(@RequestBody @Valid List<UserDto> dto) {
}
}
解法 2
第二種方式呢是自己去實作一個 List 的介面
public class ValidList<E> implements List<E> {
@Valid
private final List<E> list;
public ValidList() {
this.list = new ArrayList<E>();
}
public ValidList(List<E> list) {
this.list = list;
}
public List<E> getList() {
return list;
}
// 以下需要 Override 全部 List 的方法
@Override
public int size() {
return list.size();
}
@Override
public boolean isEmpty() {
return list.isEmpty();
}
...
}
因為要 @Override
所有方法的緣故寫起來會有點長,但是只要寫一遍就好,使用上如下,跟一般做驗證的寫法就沒兩樣的,其實概念就是前幾篇講到的巢狀驗證,要使用分組驗證也與一般用法無異,個人其實是比較偏好這種方式的
@RestController()
@RequestMapping("/v1/user")
public class UserController {
@PostMapping("")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void createUsers(@RequestBody @Valid ValidList<UserDto> dto) {
}
}
上述 ValidList
的寫法其實也可以透過 lombok
來換個簡潔一點的寫法
@Getter
public class ValidList<E> implements List<E> {
@Valid
@Delegate
private final List<E> list;
public ValidList() {
this.list = new ArrayList<E>();
}
public ValidList(List<E> list) {
this.list = list;
}
}
如果有 Set 的需求也可以按照上面作法重新實作一個 Set,這邊就不多寫了
結語
兩個做法各有利弊,簡單說明一下個人的感覺
- 第一種作法可以繼續使用原生的 List 而不用特意去建立另一個實作,使用時也可以很直覺的調用 List 出來,不過缺點就是要加的 annotation 有點多,一個忘了加這個驗證就不會生效,如果又沒有好好測試的話可能就很難注意到
- 第二種作法則是可以當作一般的類型使用,使用上的體驗是一致的,不過缺點就是必須要記得使用新建的 ValidList
其實兩種做法都可以達到目的,不過第二種作法個人認為是比較容易注意到的,事後需要的步驟越少在團隊中推廣應該也會是比較容易的