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.
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):
<properties> <java.version>11</java.version> <kotlin.version>1.4.0</kotlin.version> </properties>
<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
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:
Kotlin:
Source is available here: https://github.com/DarkAtra/jsr380-kotlin-issue/tree/workaround
Thanks again for your help Rafal G.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With