DEV Community

Mikhail
Mikhail

Posted on

Using spring-boot-starter-validation library with Kotlin

If you're using Spring Boot, you may need to validate your incoming requests. The spring-boot-starter-validation library is a good choice. However, when using Kotlin types, there is a specific consideration. This article will show you how to effectively validate JSON request body in Spring Boot with Kotlin.

Project Structure
Let's start with a simple Spring Boot project with Kotlin. The project includes two endpoints: one for a nullable type field in JSON request body and one for a non-nullable type field.

@RestController
class TemplateController {

    @PostMapping("/nullable")
    fun createNullable(
        @Valid @RequestBody request: CreateNullableRequest,
    ): String = "Hello, nullable World!"

    @PostMapping("/non-nullable")
    fun createNonNullable(
        @Valid @RequestBody request: CreateNonNullableRequest,
    ): String = "Hello, non nullable World!"
}
Enter fullscreen mode Exit fullscreen mode

Both functions use @jakarta.validation.Valid annotation and @jakarta.validation.constraints.NotBlank annotation on the fields.

data class CreateNullableRequest(

    @field:NotBlank(message = "May not be blank")
    val name: String? = null,
)

data class CreateNonNullableRequest(

    @field:NotBlank(message = "May not be blank")
    val name: String,
)
Enter fullscreen mode Exit fullscreen mode

The project also has the ErrorHandlerController for intercepting and handling validation errors.


@RestControllerAdvice
class ErrorHandlerController {

    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(MethodArgumentNotValidException::class)
    fun handleValidationException(ex: MethodArgumentNotValidException): Errors = Errors(
        errors = ex.bindingResult
            .fieldErrors
            .map {
                Error(
                    key = it.field,
                    message = it.defaultMessage ?: "Validation error"
                )
            }
    )
}
Enter fullscreen mode Exit fullscreen mode

Test Requests
Let's send the same requests to both endpoints, setting name field in the JSON request body to null and compare the responses.
Non nullable endpoint:

Request:
POST http://localhost:8080/non-nullable
Content-Type: application/json
{ "name": null }

Response:
HTTP/1.1 400 
Content-Type: application/json
Transfer-Encoding: chunked
Date: Sat, 28 Dec 2024 08:09:56 GMT
Connection: close
{
  "timestamp": "2024-12-28T08:09:56.409+00:00",
  "status": 400,
  "error": "Bad Request",
  "path": "/non-nullable"
}
Enter fullscreen mode Exit fullscreen mode

The response includes error code 400 (Bad Request) and JSON request body with some information. This response is not very useful because it doesn't provide specific details about the validation error.
Nullable endpoint:

Request:
POST http://localhost:8080/nullable
Content-Type: application/json 
{ "name": null }

Response:
HTTP/1.1 400 
Content-Type: application/json
Transfer-Encoding: chunked
Date: Sat, 28 Dec 2024 08:08:06 GMT
Connection: close
{
  "errors": [
    {
      "key": "name",
      "message": "May not be blank"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

The response also includes error code 400 (BadRequest). But the JSON body includes more specific information about the validation error, which is much more helpful for debugging and understanding the issue.
To get a more understandable error message from a non nullable endpoint, let's add a function to handle general exceptions in ErrorHandlerController.

    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ExceptionHandler(Exception::class)
    fun handleGeneralException(ex: Exception): Errors = Errors(
        errors = listOf(
            Error(
                key = ex.javaClass.simpleName,
                message = ex.message ?: "unknown error"
            )
        )
    )
Enter fullscreen mode Exit fullscreen mode

If you send an invalid request to a non-nullable endpoint again, you will receive the response:

Request:
POST http://localhost:8080/non-nullable
Content-Type: application/json
{ "name": null }

Response:
HTTP/1.1 500 
Content-Type: application/json
Transfer-Encoding: chunked
Date: Sat, 28 Dec 2024 08:28:26 GMT
Connection: close

{
  "errors": [
    {
      "key": "HttpMessageNotReadableException",
      "message": "JSON parse error: Instantiation of [simple type, class ru.epatko.template.model.CreateNonNullableRequest] value failed for JSON property name due to missing (therefore NULL) value for creator parameter name which is a non-nullable type"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

In the response, you can see error code 500 (Internal Server Error) and the error message. Although this response looks understandable to humans, it will be less understandable for another application that sends a request to our endpoint. Additional effort will be needed so that the app can parse this response as a "Bad Request" and handle it correctly.

Conclusion
As you can see, the difference between non nullable and nullable endpoints is that the application attempts to parse the JSON request body before validating the fields of the received object. If the body cannot be parsed, it throws HttpMessageNotReadableException. This is because Kotlin does not allow creating a CreateNonNullableRequest object with field name = null.
If you want to improve the consistency and clarity of your API, you should use nullable Kotlin types to validate the incoming JSON request body.
The entire project's code is available on GitHub.

Top comments (0)