bean validation not working with kotlin (JSR 380)
Asked Answered
N

3

25

so first of all i could not think of a better title for this question so i'm open for changes.

I am trying to validate a bean using the bean validation mechanism (JSR-380) with spring boot.

So i got a controller like this:

@Controller
@RequestMapping("/users")
class UserController {
    @PostMapping
    fun createUser(@Valid user: User, bindingResult: BindingResult): ModelAndView {
        return ModelAndView("someview", "user", user)
    }
}

with this being the User class written in kotlin:

data class User(
    @field:NotEmpty
    var roles: MutableSet<@NotNull Role> = HashSet()
)

and this being the test:

@Test
internal fun shouldNotCreateNewTestWithInvalidParams() {
    mockMvc.perform(post("/users")
        .param("roles", "invalid role"))
        .andExpect(model().attributeHasFieldErrors("user",  "roles[]"))
}

Invalid Roles are mapped to null.

As you can see i want roles to contain at least one item with none of the items being null. However when testing the above code no binding errors are reported if roles contains null values. It does report an error if the set is empty though. I was thinking that this might be an issue with how kotlin code compiles as the same code works just fine when the User class is written in java. Like this:

@Data // just lombok...
public class User {
    @NotEmpty
    private Set<@NotNull Role> roles = new HashSet<>();
}

Same Controller, same test.

After checking the bytecode i noticed that the kotlin version is not including the nested @NotNull annotation (see below).

Java:

private Ljava/util/Set; roles
@Ljavax/validation/constraints/NotEmpty;()
@Ljavax/validation/constraints/NotNull;() : FIELD, 0;
@Ljavax/validation/constraints/NotEmpty;() : FIELD, null

Kotlin:

private Ljava/util/Set; roles
@Ljavax/validation/constraints/NotEmpty;()
@Lorg/jetbrains/annotations/NotNull;() // added because roles is not nullable in kotlin. this does not affect validation

Now the question is why?

Here's a sample project in case you want to try some stuff.

Ninetta answered 15/9, 2018 at 13:47 Comment(5)
before \@NotEmpty just ad \@NotNull in class user and TryExemplify
still fails with java.lang.AssertionError: No errors for field 'roles[]' of attribute 'userDto' and it actually makes sense in this case as adding @NotNull to roles will only ensure that roles is never null. However i want to validate the items inside of roles.Ninetta
Yeah, I have the same problem. I think I will have to create custom validators for the time being. twitter.com/GlowinskiRafal/status/1043183121796083718Steiermark
@TommySchmidt I have created a Kotlin issue using all the details you have provided: youtrack.jetbrains.com/issue/KT-27049 - I hope you don't mind :)Steiermark
@RafalG. i dont mid at all. thanks for posting it!Ninetta
N
19

Answer (Kotlin 1.3.70)

Make sure to compile the kotlin code with jvm target 1.8 or greater and enable this feature by providing the -Xemit-jvm-type-annotations when compiling.

For Spring Boot projects you only have to do the following changes (tested with Spring Boot 2.3.3 and Kotlin 1.4.0):

  1. In your pom set the following property:
    <properties>
        <java.version>11</java.version>
        <kotlin.version>1.4.0</kotlin.version>
    </properties>
    
  2. Add <arg>-Xemit-jvm-type-annotations</arg> to the kotlin-maven-plugin:
    <build>
        <plugin>
            <artifactId>kotlin-maven-plugin</artifactId>
            <groupId>org.jetbrains.kotlin</groupId>
            <configuration>
                <args>
                    <arg>-Xjsr305=strict</arg>
                    <arg>-Xemit-jvm-type-annotations</arg>
                </args>
                <compilerPlugins>
                    <plugin>spring</plugin>
                </compilerPlugins>
            </configuration>
            <dependencies>
                <dependency>
                    <groupId>org.jetbrains.kotlin</groupId>
                    <artifactId>kotlin-maven-allopen</artifactId>
                    <version>${kotlin.version}</version>
                </dependency>
            </dependencies>
        </plugin>
    </build>
    

Sample Project

Jetbrains Release Notes


Workaround (pre Kotlin 1.3.70)

Rafal G. already pointed out that we could use a custom validator as a workaround. So here's some code:

The Annotation:

import javax.validation.Constraint
import javax.validation.Payload
import kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS
import kotlin.annotation.AnnotationTarget.CONSTRUCTOR
import kotlin.annotation.AnnotationTarget.FIELD
import kotlin.annotation.AnnotationTarget.FUNCTION
import kotlin.annotation.AnnotationTarget.TYPE_PARAMETER
import kotlin.annotation.AnnotationTarget.VALUE_PARAMETER
import kotlin.reflect.KClass

@MustBeDocumented
@Constraint(validatedBy = [NoNullElementsValidator::class])
@Target(allowedTargets = [FUNCTION, FIELD, ANNOTATION_CLASS, CONSTRUCTOR, VALUE_PARAMETER, TYPE_PARAMETER])
@Retention(AnnotationRetention.RUNTIME)
annotation class NoNullElements(
    val message: String = "must not contain null elements",
    val groups: Array<KClass<out Any>> = [],
    val payload: Array<KClass<out Payload>> = []
)

The ConstraintValidator:

import javax.validation.ConstraintValidator
import javax.validation.ConstraintValidatorContext

class NoNullElementsValidator : ConstraintValidator<NoNullElements, Collection<Any>> {
    override fun isValid(value: Collection<Any>?, context: ConstraintValidatorContext): Boolean {
        // null values are valid
        if (value == null) {
            return true
        }
        return value.stream().noneMatch { it == null }
    }
}

And finally the updated User class:

data class User(
    @field:NotEmpty
    @field:NoNullElements
    var roles: MutableSet<Role> = HashSet()
)

Altough validation works now, the resulting ConstrainViolation is slightly different. For example the elementType and propertyPath differs as you can see below.

Java:

The Java Version

Kotlin:

The Kotlin Version

Source is available here: https://github.com/DarkAtra/jsr380-kotlin-issue/tree/workaround

Thanks again for your help Rafal G.

Ninetta answered 25/9, 2018 at 19:8 Comment(9)
Seems like they've added support for this feature in kotlin 1.3.70: blog.jetbrains.com/kotlin/2020/03/kotlin-1-3-70-releasedNinetta
I confirmed that the validation is working without <arg>-Xemit-jvm-type-annotations</arg> in spring boot 2.4.4 with java 8 version and kotlin 1.4.31Lang
@Lang do you have an example for that? during my tests, <arg>-Xemit-jvm-type-annotations</arg> is still required even with kotlin 1.5.10 and spring boot 2.5.0. See: github.com/DarkAtra/jsr380-kotlin-issue/tree/…Ninetta
I've downloaded your code. um.. Why don't you add simple logging and check result? ' @PostMapping fun createUserKotlin(@Valid user: de.darkatra.jsr380kotlinissue.kotlin.User, bindingResult: BindingResult): ModelAndView { println("bindingResult.hasErrors() = ${bindingResult.hasErrors()}") return ModelAndView("someview", "user", user) } 'Lang
@Lang i've just updated the code (see the above link) and this is the log message i got: bindingResult.hasErrors() = false (make sure to run the code with mvn clean verify)Ninetta
It's weird. I got the same result with you when I run the UserControllerTest. However when I send empty POST request from POSTMAN, both cases show bindingResult.hasErrors() = true. It seems AutoConfigureMockMvc not working without <arg>-Xemit-jvm-type-annotations</arg>. How do you think about it? Actually I'm not familiar with Spring Test.Lang
you're probably seeing bindingResult.hasErrors() = true because sending an empty post request violates the @field:NotEmpty constraint on the roles field. my test case on the other hand violates the @NotNull constraint inside the MutableSet because in the test not a role is converted to null via github.com/DarkAtra/jsr380-kotlin-issue/blob/…Ninetta
It seems you are right. so we need to include <arg>-Xemit-jvm-type-annotations</arg> to validate all constraints?Lang
yes, without <arg>-Xemit-jvm-type-annotations</arg> the kotlin compiler discards all TYPE_PARAMETER annotations (e.g. annotations on generics) - you can also see the difference in the corresponding bytecode (see question)Ninetta
H
16

Try adding ? like this:

data class User(
    @field:Valid
    @field:NotEmpty
    var roles: MutableSet<@NotNull Role?> = HashSet()
)

Then the kotlin compiler should realise roles could be null, and it might honor the validation, I know little about JSR380 so i'm just guessing though.

Hassiehassin answered 25/9, 2018 at 17:20 Comment(3)
Hey, thanks for your answer! Sadly, adding a ? to Role does not make any difference. It seems to be a bug/missing feature in kotlin right now as of youtrack.jetbrains.com/issue/KT-13228. I will update my question and point to a workaround until it is fixed.Ninetta
This is best answer! Thank you!!!Courcy
Making type class nullable just in sake of validation is not really clean approach.Montage
M
1

I had a similar problem. The solution was to add this dependency:

 implementation "org.springframework.boot:spring-boot-starter-validation"

Without this dependency, the REST controller was working but no exception was thrown when the javax.validation bean constraints were not honored. Now it throws MethodArgumentNotValidException.

Myoglobin answered 29/8, 2022 at 13:24 Comment(1)
What a lifesaver, thank you for sharing this! I thought it was due to some Kotlin issue.Massa

© 2022 - 2024 — McMap. All rights reserved.