Customising form validation in Javalin using Valiktor
Since about a month I’m developing some microservices using Javalin, a small, simple and lightweight web-framework that started as a fork from SparkJava. By now it’s a ground-up rewrite, influenced by the Javascript framework koa.js. Don’t let the stuff like Either scare you, we use the Arrow library to make our life a bit more easy :)
When requesting user input from a form, it’s evident that we need to validate our input. Javalin has its own way of doing so (from the docs):
// validate a json body:
val myObject = ctx.bodyValidator<MyObject>()
.check({ it.myObjectProperty == someValue, "Oops, an error!" })
.get()
Resulting in the output:
"Request body as MyObject invalid - Oops, an error!"
As you see, not a lot of customisation to alter your error. So when I got the request to return an error in another format to satisfy our front-end application:
{ "fieldname": "invalid",
"fieldname": "required",
...
}
I had to find another solution. This is where Valiktor came to the rescue, using it is easy.
Add the dependency to your application
<!-- https://mvnrepository.com/artifact/org.valiktor/valiktor-core -->
<dependency>
<groupId>org.valiktor</groupId>
<artifactId>valiktor-core</artifactId>
<version>0.8.0</version>
</dependency>
Alter the (translation) messages to your needs
For example create the file: ./src/main/resources/messages_en.properties
org.valiktor.constraints.NotEmpty.message=required
org.valiktor.constraints.NotBlank.message=required
org.valiktor.constraints.Email.message=invalid
Create your custom validator class
class FormValidator {
companion object Factory {
fun validateInput(input: UserData) : Either<BadState, UserData> =
try {
Either.Right(validate(input) {
validate(UserData::name).isNotBlank()
validate(UserData::surname).isNotBlank()
validate(UserData::email).isNotBlank().isEmail()
})
} catch (ex: ConstraintViolationException) {
Either.Left(ValidationError( ex.constraintViolations
.mapToMessage(baseName = "messages", locale = Locale.ENGLISH)
.map { "${it.property}: ${it.message}" } ))
}
}
}
The BadState class
sealed class BadState
data class NotAuthorised(val reason : String) : BadState()
data class OtherError(val reason : String) : BadState()
// This is the custom bad state we need :)
data class ValidationError (val errors : List<String>) : BadState()
Then in your controller:
fun handleYourPost(ctx: Context) {
when (val result = createUser(ctx.body<UserData>())) {
is Either.Right -> {
ctx.status(201)
ctx.json(result.b)
}
is Either.Left -> {
when (val error = result.a) {
is ValidationError -> {
ctx.status(400)
ctx.json(error.errors)
}
is OtherError -> {
ctx.status(500)
ctx.json("something went wrong...")
}
}
}
}
}
fun createUser(userInput: UserData): Either<Any, UserData> {
return Either.fx<BadState, UserData>{
val u = FormValidator.validateInput(userInput).bind() //Do field validation
...
}
}
So now we’re happy to see the 400 error showing all fields and their errors in a json array.
{ "surname": "required",
"email": "invalid"
}