RequestbodyをJsonで受けて、Kotlinのdata classにセットするケースで、data classのプロパティがNon-Nullableだと、マッピングするJSONフィールドが無いとエラーになります。(当たり前ですが)
data classのプロパティに Bean validation@NotNull(message="")
を付けても検証されない。
NotNullチェックのために、data class のプロパティを Nullable にするのも違和感があったので、 例外ハンドリングで対応してみた。
import com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.http.converter.HttpMessageNotReadableException
import org.springframework.validation.FieldError
import org.springframework.web.bind.MethodArgumentNotValidException
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.ResponseStatus
import org.springframework.web.bind.annotation.RestControllerAdvice
import org.springframework.web.context.request.WebRequest
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler
@RestControllerAdvice
class CustomeExceptionHandler : ResponseEntityExceptionHandler() {
/**
* Non-Nullableなプロパティにマッピングするパラメータが不正だった場合のエラーメッセージを作成する
* */
override fun handleHttpMessageNotReadable(
ex: HttpMessageNotReadableException,
headers: HttpHeaders,
status: HttpStatus,
request: WebRequest
): ResponseEntity<Any> {
val body = when (val cause = ex.cause) {
is MissingKotlinParameterException -> {
cause.path.joinToString(",") { it.fieldName }
.let { mapOf(Pair("入力パラメータがありません", it)) }
}
is InvalidFormatException -> {
cause.path.joinToString(",") {
"type of ${it.fieldName} should be ${cause.targetType}. but value was ${cause.value}"
}.let { mapOf(Pair("入力パラメータの型が一致しません", it)) }
}
else -> mapOf(Pair("予期せぬエラー ${cause?.javaClass?.name}", ex.localizedMessage))
}
return handleExceptionInternal(ex, body, headers, status, request)
}
/**
* RequestBodyのValidationメッセージをレスポンスに出力するためにフィールドとメッセージをマッピングします
* */
override fun handleMethodArgumentNotValid(
ex: MethodArgumentNotValidException,
headers: HttpHeaders,
status: HttpStatus,
request: WebRequest
): ResponseEntity<Any> {
val body = ex.bindingResult.allErrors.associateBy({ it.let { it as FieldError }.field }, { it.defaultMessage })
return handleExceptionInternal(ex, body, headers, status, request)
}
override fun handleExceptionInternal(
ex: Exception,
body: Any?,
headers: HttpHeaders,
status: HttpStatus,
request: WebRequest
): ResponseEntity<Any> {
val responseBody = body ?: CustomeErrorResponse(ex.message ?: ex.localizedMessage)
return super.handleExceptionInternal(ex, responseBody, headers, status, request)
}
}
data class CustomeErrorResponse(val message: String)
余計な関数も記載してるが、 ResponseEntityExceptionHandler
を継承した、例外ハンドリングクラスで handleHttpMessageNotReadable
関数をオーバーライドしている。
このメソッドがハンドリングしている、HttpMessageNotReadableException
は、Non-Nullableプロパティにセット出来なった場合に発生する。
更にこの例外の原因は MissingKotlinParameterException
であり、この例外オブジェクトからセットされていないJsonフィールドが分かるので、この情報をレスポンスに含めるようにしている。
このハンドリング処理は未検証な点もあるが、Non-NullableプロパティとマッピングされていなJSONフィールドが複数有っても最初の1つのフィールドしか取れない。
MissingKotlinParameterException.path
は、リスト型だったので複数のパラメータを持っていると考えて、joinToString
でまとめて出力したかったが、実装方法が不明。
とにかく、これでNon-Nullableなプロパティに対する例外ハンドリングも出来るようになった。
更に、Jsonフィールドが、マッピングするdata classの型と一致しない場合は、InvalidFormatException
でハンドリングすることもできた。