13

I have quite a few projects that is slowly being migrated from Java to Kotlin, but I'm facing a problem when changing from Java POJO to Kotlin data classes. Bean validation stops working in REST controllers. I have created a very simple project directly from https://start.spring.io to demonstrate the failure.

@SpringBootApplication
class RestValidationApplication

fun main(args: Array<String>) {
    runApplication<RestValidationApplication>(*args)
}

@RestController
class Controller {

    @PostMapping
    fun test(@Valid @RequestBody request: Test) {
        println(request)
    }
}

data class Test(@field:NotNull val id: String)

and gradle:

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
    id("org.springframework.boot") version "2.6.1"
    id("io.spring.dependency-management") version "1.0.11.RELEASE"
    kotlin("jvm") version "1.6.0"
    kotlin("plugin.spring") version "1.6.0"
}

group = "com.example"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_17

repositories {
    mavenCentral()
}

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-validation")
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
    testImplementation("org.springframework.boot:spring-boot-starter-test")
}

tasks.withType<KotlinCompile> {
    kotlinOptions {
        freeCompilerArgs = listOf("-Xjsr305=strict")
        jvmTarget = "17"
    }
} 

Sending a request does not trigger the bean validation, but it throws a HttpMessageNotReadableException because id is NULL.

curl -X POST http://localhost:8080 -H "Content-Type: application/json" -d "{}"

I have tried both @Valid and @Validated and using @get:NotNull on the attributes, but nothing works. I see many others have the same problem and using a Java class from a Kotlin REST controller works. Also changing to a non data class makes validation works. Anyone know if it's possible to get bean validation working with Kotlin data classes?

6
  • I might be wrong, but maybe the controller class should also be annotated with @Validated ... Commented Dec 3, 2021 at 14:17
  • It should work as-is. Have you tried using Spring Boot 2.5.X instead or adding hibernate-validator dependency? Commented Dec 3, 2021 at 15:26
  • I've tried Spring boot 2.4.13, 2.5.7 and 2.6.1. Also tried to replace org.springframework.boot:spring-boot-starter-validation with org.hibernate.validator:hibernate-validator:7.0.1.Final. And of course both Kotlin 1.5.x and 1.6.0. Commented Dec 3, 2021 at 19:50
  • Have you tried other Java versions? Commented Dec 4, 2021 at 1:29
  • 1
    Doesn't the @NotNull (assuming this is the JSR-303 one) actually create a null check in the generated constructor for the data class? Which would basically throw the error upon construction, which wouldn't allow to reach validation at all as it will fail much earlier in the process. Please add the full stacktrace on the server you get when this happens, as I think it fails upon object construction which basically stops the whole processing (which also explains why it would work on a regular java class). Commented Dec 16, 2021 at 7:48

5 Answers 5

11

Make the field nullable even its not, and then add @field:NotNull

data class Test(@field:NotNull val id: String?) 

This will stop kotlin validation to happen before javax

Sign up to request clarification or add additional context in comments.

1 Comment

Worked like a charm, thanks
4

Do not use data class for validation

when you use data class you need to add @field:{} before annotation or you can use class without data class .

data class:

class AuthorInput(
    @field:NotBlank @field:Size(max = 20, min = 3) val first_name: String? = null,
    @field:NotBlank @field:Size(max = 20, min = 3) val last_name: String? = null,
    @field:NotEmpty val role: List< String?>? = null

){
    
    
}

only class:

    class AuthorInput {
    @NotBlank
    @Size(max = 20, min = 3)
    val first_name: String? = null

    @NotBlank
    @Size(max = 20, min = 3)
    val last_name: String? = null

    @NotEmpty
    val role: List<@NotBlank String?>? = null


}

1 Comment

both for class and data class @field annotation is required. Found this detailed explanation very useful. stackoverflow.com/a/59925322
2

You have to add @get:Valid before. For example:

data class Location(
    @get:Valid @get:DecimalMin("-90") @get:DecimalMax("90")  val lat: Double,
    @get:Valid @get:DecimalMin("-180") @get:DecimalMax("180") val lng: Double
)

1 Comment

This helped in my case.
1

The issue has been fixed in Kotlin version 1.6.10. https://github.com/JetBrains/kotlin/releases/tag/v1.6.10. After upgrading the exception is now org.springframework.web.bind.MethodArgumentNotValidException.

1 Comment

The real reason your code is causing a HttpMessageNotReadableException instead of MethodArgumentNotValidException in the test scenario you described is that the request is breaking at the Test object creation. So, Spring has no chance to apply any validation rules. So, the correct answer to this question is @wherath 's anwser here
0

I think you are just missing @Validated annotation on top of your controller class.

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.validation.FieldError
import org.springframework.validation.annotation.Validated
import org.springframework.web.bind.MethodArgumentNotValidException
import org.springframework.web.bind.annotation.*
import org.springframework.web.context.request.WebRequest
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler
import java.time.LocalDateTime
import javax.validation.Valid
import javax.validation.constraints.NotEmpty


@SpringBootApplication
class BootValidationApplication

fun main(args: Array<String>) {
    runApplication<BootValidationApplication>(*args)
}

@RestController
@RequestMapping("/api/v1")
@Validated
class CustomerController {
    @PostMapping("/customer")
    fun createCustomer(@Valid @RequestBody customer: Customer): ResponseEntity<Customer> {
        return ResponseEntity.ok(customer)
    }
}


@ControllerAdvice
class CustomerExceptionHandler : ResponseEntityExceptionHandler() {

    override fun handleMethodArgumentNotValid(
        ex: MethodArgumentNotValidException,
        headers: HttpHeaders,
        status: HttpStatus,
        request: WebRequest
    ): ResponseEntity<Any> {

        val fieldErrors: List<FieldError> = ex.fieldErrors
        val errorMapping = fieldErrors.associate { it.field to it.defaultMessage }

        val errorDetails = ErrorDetails(
            timestamp = LocalDateTime.now(),
            message = "Validation Failed",
            details = errorMapping
        )
        return ResponseEntity(errorDetails, HttpStatus.BAD_REQUEST)
    }
}

data class Customer(
    @field:NotEmpty(message = "Mandatory field: Name is missing in Request Body") val name: String
)

data class ErrorDetails(val timestamp: LocalDateTime, val message: String, val details: Map<String, String?>)

The output should look something like this enter image description here

8 Comments

The problem is that the error thrown is: org.springframework.http.converter.HttpMessageNotReadableException. I would like it to be org.springframework.web.bind.MethodArgumentNotValidException to be able to handle validation errors properly.
In that case, you need to capture the expectation by implementing the ConstraintValidator<> Interface and throwing the error message.
updated the post for your reference
It would have to be override fun handleHttpMessageNotReadable. And still that prints a horrible error message. Not something you can expose in a public API. I would like error message to be short and precise. We would often add a message to the validation annotations like: NotEmpty(message = "Please provide an ID"). Also parsing the error message for fields, seems like a bad solution, when this is build into Spring when using Java.
The @Validated on the class level is for a whole different purpose as the @Valid on the method level. It also leads to a totally different handling of errors, so suggestion to do so on the web layer is actually wrong.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.