Skip to content

バリデーションエラーのハンドリング

実装

pom.xml

xml
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>

controller/AuthController.kt

kotlin
package nob.example.easyapp.controller

import jakarta.validation.Valid
import nob.example.easyapp.controller.model.LoginRequest
import nob.example.easyapp.controller.model.LoginResponse
import nob.example.easyapp.controller.model.MeRequest
import nob.example.easyapp.controller.model.MeResponse
import nob.example.easyapp.service.AuthService
import nob.example.easyapp.service.model.LoginInModel
import nob.example.easyapp.service.model.MeInModel
import org.springframework.web.bind.annotation.*

/**
 * 認証コントローラーです。
 */
@RestController
@RequestMapping("/api/v1")
class AuthController(private val authService: AuthService) {

    /**
     * 認証処理を呼び出します。
     */
    @PostMapping("/login")
    fun login(@RequestBody @Valid req: LoginRequest): LoginResponse {
        return LoginResponse(authService.login(LoginInModel(req.name, req.password)).valid)
    }
}

controller/model/AuthModel.kt

kotlin
package nob.example.easyapp.controller.model

import jakarta.validation.constraints.NotBlank

/**
 * 認証向けのリクエストモデルです。
 */
data class LoginRequest(

    /**
     * ユーザ名
     */
    @NotBlank(message = "{loginRequest.name.NotBlank}")
    val name: String,

    /**
     * パスワード
     */
    val password: String
)

/**
 * 認証向けのレスポンスモデルです。
 */
data class LoginResponse(

    /**
     * 認証可否
     */
    val valid: Boolean
)

resources/ValidationMessages.properties

shell
# エラーメッセージの辞書ファイルです。

loginRequest.name = ユーザ名
loginRequest.name.NotBlank = {loginRequest.name}を入力してください。

handler/MethodArgumentNotValidExceptionHandler.kt

kotlin
package nob.example.easyapp.handler

import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.MethodArgumentNotValidException
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.RestControllerAdvice

/**
 * バリデーションによる例外のハンドラです。
 */
@RestControllerAdvice
class MethodArgumentNotValidExceptionHandler {


    @ExceptionHandler(MethodArgumentNotValidException::class)
    fun handleMethodArgumentNotValidException(e: MethodArgumentNotValidException): ResponseEntity<MethodArgumentNotValidExceptionResponse> {

        val messageList: List<String?> = e.allErrors.map { it.defaultMessage }
        return ResponseEntity(MethodArgumentNotValidExceptionResponse(messageList), HttpStatus.BAD_REQUEST)
    }

    /**
     * MethodArgumentNotValidException発生時のレスポンスボディです。
     */
    data class MethodArgumentNotValidExceptionResponse(

        /**
         * エラーメッセージのリスト
         */
        val messageList: List<String?>
    )
}

テスト

AuthControllerTest.kt

kotlin
package nob.example.easyapp.controller

import nob.example.easyapp.controller.model.LoginRequest
import nob.example.easyapp.handler.MethodArgumentNotValidExceptionHandler
import nob.example.easyapp.service.AuthService
import nob.example.easyapp.service.model.LoginInModel
import nob.example.easyapp.service.model.LoginOutModel
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.mockito.kotlin.whenever
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest
import org.springframework.http.MediaType
import org.springframework.test.context.bean.override.mockito.MockitoBean
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
import tools.jackson.databind.ObjectMapper

/**
 * AuthControllerのテストクラスです。
 */
@WebMvcTest(AuthController::class)
class AuthControllerTest {

    @Autowired
    lateinit var mockMvc: MockMvc

    @MockitoBean
    lateinit var authService: AuthService

    var objectMapper: ObjectMapper = ObjectMapper()

    @Test
    fun testLoginInvalidName() {

        // リクエスト作成
        val req = LoginRequest("", "passwd")

        // serviceのモック化
        whenever(authService.login(LoginInModel(req.name, req.password))).thenReturn(LoginOutModel(true))

        // API呼び出し
        val result = mockMvc.perform(
            post("/api/v1/login")
                .content(objectMapper.writeValueAsString(req))
                .contentType(MediaType.APPLICATION_JSON)
        )
            .andExpect(status().isBadRequest())
            .andReturn()
        // 結果の検証
        assertThat(result.response.contentAsString).isEqualTo(
            objectMapper.writeValueAsString(
                MethodArgumentNotValidExceptionHandler.MethodArgumentNotValidExceptionResponse(listOf("ユーザ名を入力してください。"))
            )
        )
    }
}

API動確

$ curl -X POST -H 'Content-Type: application/json' -d '{"name": "", "password": "passwd"}' localhost:8080/api/v1/login
{"messageList":["ユーザ名を入力してください。"]}