Kotlin Quick Tip: Annotation targets
If you -- like me -- are a sucker for reducing boilerplate and are using JSR-303 - Bean Validation (I use it as part of Spring Boot) then you may have noticed how it can help you reduce validation boilerplate code. We can define REST endpoints on our application, which consume objects, that we can have Spring Boot automatically validate if we use the @Valid annotation.
Let's assume we have a Person class, like below:
https://gist.github.com/jwalgemoed/f18417a2d0488a125df9fd1298f9c290
We've got this class all worked out. It has a truckload of properties and all require validation. We are going to operate under the premise that most persons will have a first name and a last name, so it can't be empty or null. Apparently, we chose a database from the stone age -- so we can store persons with first and last names shorter than 20 characters in our database.
Having all of this set up, we can define a REST endpoint to consume a person (it sounds illegal when you say this out loud) like so:
https://gist.github.com/jwalgemoed/367c2d5889105e684e70e3029cc78429
Apart from the odd storage provider, all seems to be in order. We added the parameters, we added the mapping,
we made sure to annotate the request body with @Valid, should work fine, right? Wrong. If we were to create a person with a last name longer than 20 characters, such as "Justin Timbertimbertimberlake", we will get an error from the database chastising us on why we are allowing persons through with unvalidated last names.
What's wrong? A quick look at the decompiled code may shed some light on the situation:
https://gist.github.com/jwalgemoed/b305451ae52bcee9aa91aa17617bc066
The annotations are being added to the constructor arguments. Under normal circumstances that'd be fine, but sadly Hibernate Validator (bundled with Spring) will not process these annotations on the constructor. Unlike Java, Kotlin (data) classes allow you to define fields in the constructor definition of the class. This means they aren't actual fields in the Kotlin class. Kotlin will generate a getter (for public), setter (for var) and backing field for the properties defined in the Kotlin class (as val/var). So how can we persuade Kotlin to help out a bit by placing the annotations on Hibernate Validators favourite location?
Annotation targets
The fix is actually rather simple. We just need to explain to Kotlin that the annotations need to go elsewhere. So, when we rewrite our code to look like the snippet below, we are instructing Kotlin to not apply the annotations to the constructor arguments, but to the generated fields.
https://gist.github.com/jwalgemoed/74c2cf1c2ec39635b8f328654fb62c67
Trying to add "Justin Timbertimbertimberlake" now, will generate a validation error (length must be between 1 and 20), leaving us with a much happier database.
As you probably expected by now, for this scenario Kotlin defines three flavours of targets:
https://gist.github.com/jwalgemoed/38895fb069f95797a1d1fd8d33f3921a
Good to know if you want to prevent some head scratching when applying framework annotations on our brand new Kotlin codebases.
More info on annotation targets is available in the great Kotlin language reference.