Kotlin doesn't play nice with Beans Validation annotations and this needs to be addressed

Spring and Spring Boot are what I would call “kotlin-friendly” frameworks. However, the beans validation aspect specifically just doesn’t play nice with kotlin. Here’s why.

Let’s assume the following data class:

data class UserDTO(
    @jakarta.validation.constraints.Size(max = 10)
    val username: String
)

… and the following REST controller:

@RestController
class UserRestController {

    @PostMapping("/users")
    @ResponseBody
    fun saveUser(@RequestBody @jakarta.validation.Valid  userDTO: UserDTO): Int {
        return 0 // doesn't matter
    }
}

Let’s post some data to this endpoint:

{
    "username": "1234567890ABC"
}

Now, given the @Size annotation on the username property, I would assume that the request would fail due to the username being too long. The validation should kick in, throwing an appropriate exception, the client should receive some 4xx HTTP response code. But that doesn’t happen.

Instead, the request above is accepted by the server and the validation is ignored.

After some investigation (because debugging runtime annotation processors is always fun, right?) it turns out that the Kotlin compiler takes the @Size annotation and places it on the constructor parameter. The beans validation API doesn’t find it there. From the point of view of the beans validation API, there is no annotation.

The way to fix this is to change the @Size to @field:Size to force the kotlin compiler to put the annotation on the field.

This may seem like a minor thing. But it’s something we as developers constantly have to keep in mind. It’s not just syntactically unpleasant. If you don’t know about this issue, you’ll not even think about this, and then you’re hit with the big surprise that your validation doesn’t work. And the original code totally looks like it should work.

Ways I see to fix this:

  • The kotlin spring compiler plugin could relocate the annotations of the bean validation package to the fields instead of the constructor parameter.
  • The beans validation API spec could be changed to check for constructor parameter annotations as well.
  • The beans validation API may provide a plug-in mechanism that allows us to have a Beans-Validation-Kotlin addon that finds the annotations on the primary constructor arguments.
  • Somebody goes ahead and implements the beans validation API from scratch, in Kotlin, for Kotlin projects.

I tried to report this on the Kotlin YouTrack but it was quickly dismissed without further consideration.

Did anybody else get hit by this?

1 Like

Generally it’s a fact that anyone using a framework based on annotations will have to know.
I found this out with Dagger in a similar fashon.

Said that, wiritng a unit test is usually enough to detect the issue, once you have that figuring our what’s going on should be quite easy.

Of the solutions you mentioned some are on the maintainers of Spring, it might be worth asking them, I’ve no idea to what extent they support Kotlin directly though

Does the @jakarta.validation.constraints.Size annotation ever make sense on another target besides field? They should probably lock down the annotation to only target fields if not.

It sounds like the reason for the issue was that it wasn’t obvious to the code authors that the annotations were being applied incorrectly.

Someone could create a validation rule that flags annotations where without a use-site target where there could be multiple use-sites. That solution is something a third party can create, and users could enable individual projects to select annotations based on their needs (or just warn/error on all annotations).

1 Like

I think you need to place the annotation on the getter. Try the below:

 @get:jakarta.validation.constraints.Size(max = 10) val username: String

See: Java Annotations | Kotlin Documentation (kotlinlang.org)

I don’t think this is possible, because the validation happens after the object is initialized, right? So the constructor params no longer hold a value. All you could do is try to associate the annotation on the constructor param with a matching field/getter, but then that’s basically what you already said in your first dot point.

This would absolutely solve the problem. Another option potentially would be for Kotlin to add some kind of “default target” option for annotations, since yeah, Kotlin class constructor params are very overloaded for annotation targets. The default is constructor param, but idk how often that makes sense. They can’t change the default though, cause that’d break everyone’s existing code. So maybe a new option to say “this annotation by default should target the field, not the constructor param”. That’s only if locking down the annotation targets doesn’t solve the issue, which it probably would.

1 Like