Death by Quadratic LogoMenu

Validate parameterized collections using annotations with Spring Boot and Kotlin

Kotlin is a wonderful language that's enjoyed widespread adoption, but there are still a few gotchas when using it with massive frameworks like Spring. In this post, we're going to explore one such gotcha—validation of parameterized collections like List<String>—and a simple solution.

TLDR: Jump to the solution.

So, what's the problem?

Let's say you have a simple REST POST endpoint that accepts a request body representing a Person and simulates creating that Person by returning a dummy response. Here's the code:

data class Person(
    val name: String,
    val namesOfChildren: List<String>
)

@RestController
class RestEndpoints {
    @PostMapping
    fun createPerson(@RequestBody person: Person) = ResponseEntity.ok("Person created successfully!")
}

Okay, big deal. The Person data class takes a name and a list of names of children. Right now, if you call this endpoint with an empty body, you'll get a 400 Bad Request as expected:

$ curl -X POST http://localhost:8080/ -H 'Content-Type: application/json' -d '{}'
{"timestamp":"2024-03-13T17:32:26.393+00:00","status":400,"error":"Bad Request","path":"/"}

If you look at your application logs, you'll see a message like this:

JSON parse error: Instantiation of [simple type, class com.deathbyquadratic.blogs.validateparameterizedcollectionsannotationsspringbootkotlin.Person] value failed for JSON property name due to missing (therefore NULL) value for creator parameter name which is a non-nullable type]

This is because the JSON parser requires us to pass parameters for all non-nullable types. In our case, those are name and namesOfChildren. This level of validation is something we kind of get for free with a strong type system.

However, nothing prevents us from passing empty strings and lists like so:

$ curl -X POST http://localhost:8080/ -H 'Content-Type: application/json' -d@- <<EOF
{
  "name": "",
  "namesOfChildren": []
}
EOF
Person created successfully!

Let's prevent that by using the @NotBlank and @NotEmpty annotations. We'll also need to add @Valid to the request body so that Spring knows to run the validators on it. Here's the revised code:

data class Person(
    @field:NotBlank
    val name: String,
    @field:NotEmpty
    val namesOfChildren: List<@NotBlank String>
)

@RestController
class RestEndpoints {
    @PostMapping
    fun createPerson(@Valid @RequestBody person: Person) =
        ResponseEntity.ok("Person created successfully!")
}

The field: prefix is necessary in Kotlin due to its terse syntax where name and namesOfChildren are both constructor parameters as well as backing fields. We want to validate the backing fields. It's not necessary inside the List<String>, though, since annotation usage there is unambiguous: it can only refer to type annotation.

Alrighty, let's give it a whirl:

$ curl -X POST http://localhost:8080/ -H 'Content-Type: application/json' -d@- <<EOF
{
  "name": "",
  "namesOfChildren": []
}
EOF
{"timestamp":"2024-03-13T17:46:06.469+00:00","status":400,"error":"Bad Request","path":"/"}

Yay! We're getting a 400 Bad Request now like we should. Okay, let's pass in both a name and some children's names as well:

$ curl -X POST http://localhost:8080/ -H 'Content-Type: application/json' -d@- <<EOF
{
  "name": "John Doe",
  "namesOfChildren": ["Bobby Doe", "Sally Doe"]
}
EOF
Person created successfully!

It works as expected. Oh yeah, we also added List<@NotBlank String>, so the list itself should fail to validate if we pass an empty string as one of its items. Let's try it:

$ curl -X POST http://localhost:8080/ -H 'Content-Type: application/json' -d@- <<EOF
{
  "name": "John Doe",
  "namesOfChildren": ["Bobby Doe", "Sally Doe", ""]
}
EOF
Person created successfully!

Ah, crap... it validated, and it shouldn't have.

Where it all goes wrong, and how to fix it

The problem is that the Kotlin compiler, by default, doesn't retain type annotations when compiling to JVM bytecode. Type annotations are simply annotations that are applied to a type like how @NotBlank String above applies the @NotBlank annotation to the String type.

Since Kotlin drops type annotations during compilation, the Hibernate Validator which inspects annotations at runtime can't find them. From the validator's point of view, they never existed to begin with.

Thankfully, the solution is simple. As of Kotlin 1.4.0, the Kotlin compiler takes a flag named -Xemit-jvm-type-annotations, which will retain the type annotations during compilation. Since Kotlin 1.4.0 is ancient at this point, and you should be using a higher version anyway, you should have no problem adding this compiler flag.

If you're using Maven, you can add compiler flags to the kotlin-maven-plugin like so:

<plugin>
  <groupId>org.jetbrains.kotlin</groupId>
  <artifactId>kotlin-maven-plugin</artifactId>
  <configuration>
    <args>
      <arg>-Xemit-jvm-type-annotations</arg>
    </args>
  </configuration>
</plugin>

If you use IntelliJ IDEA to build your project, it may not respect your kotlin-maven-plugin settings when building. You can either configure the Kotlin compiler inside of IntelliJ IDEA to also compile with the -Xemit-jvm-type-annotations flag, or have IntelliJ IDEA run your Maven build as a pre-build step.

Appendix

The source code referenced in this blog post is available on GitHub. In particular, this commit contains all of the meaningful changes.

Special thanks to Alex MacArthur for proofreading this post.

Published March 13, 2024 by Jacob Chappell

Jacob Chappell
Jacob Chappell is a Senior Software Engineer working for Ramsey Solutions to bring life change to the financial industry. He holds a Master's degree in Computer Science from the University of Kentucky and has been developing software since 2005.