要約
- Spring BootでAPIを開発しており、パスパラメタへバリデーションを設定した
- バリデーションを通過できなかったときにレスポンスボディへパラメタ名と理由を設定したい
- しかしパラメタ名の取得でつまづいたのでその記録を記載
Controllerの例
@RestController
@Validated
@RequestMapping("/api/")
public class OrderController {
@PostMapping("/order/{pathOrderId}")
public ResponseEntity<OrderResponse> postOrder(
@PathVariable(name = "pathOrderId") @Size(min = 1, max = 6) String pathOrderId,
@RequestBody @Validated OrderRequest request){
// ...
}
}
この例では、/api/order/1234567
へリクエストするとpathOrderIdのSize上限のため通過できずConstraintViolationExceptionがスローされる。
このとき、@RestControllerAdviceで捕捉してエラー情報をボディに設定して応答したい。
たとえばこのようなレスポンスにしたいとする。
{
"item": "pathOrderId",
"reason": "1 から 32 の間のサイズにしてください"
}
ConstraintViolationExceptionからこれらの情報を取得しようと考えていたが、目当てのものは存在せず思ったよりも難しいことが分かった。
ConstraintViolationから取得を試みる
ConstraintViolationException#getConstraintViolations() でConstraintViolationを取得できる。
このなかの#getPropertyPath()が使用できそうなので、試してみる。
インターフェース ConstraintViolation
試したコード
@RestControllerAdvice
public class ControllerExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<Object> handleConstraintViolationException(ConstraintViolationException ex, WebRequest request) {
HttpHeaders headers = new HttpHeaders();
List<OrderResponseValidationResult> relList = new ArrayList<>();
ex.getConstraintViolations().forEach(cv -> {
OrderResponseValidationResult rel = new OrderResponseValidationResult(cv.getPropertyPath().toString(), cv.getMessage());
relList.add(rel);
});
oRes.setValidationResults(relList);
return handleExceptionInternal(ex, oRes, headers, HttpStatus.BAD_REQUEST, request);
}
public class OrderResponseValidationResult {
String item;
String reason;
public OrderResponseValidationResult() {};
public OrderResponseValidationResult(String item, String reason){
this.item = item;
this.reason = reason;
}
}
結果
/api/order/1234567
へリクエストすると、パラメタ名が取得できていない。
{
"item": "postOrder.arg1",
"reason": "1 から 32 の間のサイズにしてください"
}
対応方法
調べてみたが簡単に取得するすべがないように見えるので、次のような方法に妥協した。
案1:パラメタ名へ置換する
- "postOrder.arg1"をKeyにしたMapを用意する
- Valueへ実際のパラメタ名を設定する
@RestControllerAdvice
public class ControllerExceptionHandler extends ResponseEntityExceptionHandler {
// パスパラメタ、クエリパラメタを変換するためのMap
private static final Map<String, String> PARAMS = new HashMap<String, String>() {
{
// 今後パラメタが増えたらここに追加する
put("postOrder.arg1", "pathOrderId");
}
};
// パラメタが取得できた場合は置換したパラメタ名をreturn
public String getParameterName(String name){
return PARAMS.get(name) != null ? PARAMS.get(name) : name;
}
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<Object> handleConstraintViolationException(ConstraintViolationException ex, WebRequest request) {
HttpHeaders headers = new HttpHeaders();
List<OrderResponseValidationResult> relList = new ArrayList<>();
ex.getConstraintViolations().forEach(cv -> {
// パスパラメタを置換してレスポンスへセット
OrderResponseValidationResult rel = new OrderResponseValidationResult(getParameterName(cv.getPropertyPath().toString()), cv.getMessage());
relList.add(rel);
});
oRes.setValidationResults(relList);
return handleExceptionInternal(ex, oRes, headers, HttpStatus.BAD_REQUEST, request);
}
これで当初期待したレスポンスが得られた。望ましい方法ではないがとりあえず良しとする。
{
"item": "pathOrderId",
"reason": "1 から 32 の間のサイズにしてください"
}
案2:パスパラメタのバリデーションの失敗は詳細をレスポンスしない
設計を変えて、ConstraintViolationExceptionではバリデーションの詳細を応答するのはやめてしまう。
@RestControllerAdvice
public class ControllerExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<Object> handleConstraintViolationException(ConstraintViolationException ex, WebRequest request) {
HttpHeaders headers = new HttpHeaders();
List<OrderResponseValidationResult> relList = new ArrayList<>();
ex.getConstraintViolations().forEach(cv -> {
// パスパラメタを置換してレスポンスへセット
OrderResponseValidationResult rel = new OrderResponseValidationResult(getParameterName(null, "Invalid parameters");
relList.add(rel);
});
oRes.setValidationResults(relList);
return handleExceptionInternal(ex, oRes, headers, HttpStatus.BAD_REQUEST, request);
}
結果は次のとおり。
{
"item": null,
"reason": "Invalid parameters"
}
まとめ
- 妥協した方法で今回は実装するが、もっとスマートな方法で実装したい