Custom Hibernate constraint in Kotlin
Quarkus uses Hibernate Validator for validation. This means that the correct way to do custom field validation is to create a custom constraint. The steps to do this are well documented. However, since I am still learning Kotlin, it was not trivial for me to convert the sample code to Kotlin, especially the annotation part.
Each constraint must have a corresponding annotation that can be used to apply a constraint to a field:
import javax.validation.Constraint
import javax.validation.Payload
import kotlin.annotation.AnnotationTarget.*
import kotlin.reflect.KClass
@Target(FIELD, FUNCTION, VALUE_PARAMETER, ANNOTATION_CLASS, TYPE_PARAMETER)
@Retention(AnnotationRetention.RUNTIME)
@Constraint(validatedBy = [SloPhoneValidator::class])
@MustBeDocumented
annotation class SloPhone(
val message: String = "{org.example.validators.SloPhone.message}",
val groups: Array<KClass<*>> = [],
val payload: Array<KClass<out Payload>> = []
)
The most important part of the constraint is of course the validator class:
import com.google.i18n.phonenumbers.NumberParseException
import com.google.i18n.phonenumbers.PhoneNumberUtil
import javax.validation.ConstraintValidator
import javax.validation.ConstraintValidatorContext
class SloPhoneValidator : ConstraintValidator<SloPhone, String> {
override fun isValid(
value: String?,
constraintContext: ConstraintValidatorContext?
): Boolean {
if (value == null) {
return true
}
val phoneUtil = PhoneNumberUtil.getInstance()
return try {
val slovenianNumberProto = phoneUtil.parse(value, "SI")
phoneUtil.isValidNumber(slovenianNumberProto)
} catch (e: NumberParseException) {
false
}
}
}
To implement this validation example, I am using Google's library for phone number parsing, formatting and validation:
<dependency>
<groupId>com.googlecode.libphonenumber</groupId>
<artifactId>libphonenumber</artifactId>
<version>8.13.5</version>
</dependency>
The annotation class also references the error message, which must be placed in the ValidationMessages.properties
resource:
org.example.validators.SloPhone.message=must be a valid Slovenian phone number
This is already enough to use the constraint on a class field:
class ContactDetails {
@NotNull
@SloPhone
var mobilePhone: String? = null
var workPhone: String? = null
}
When called if an invalid phone number a Quarkus REST endpoint would now respond with:
{
"title": "Constraint Violation",
"status": 400,
"violations": [
{
"field": "autoValidation.person.mobilePhone",
"message": "must be a valid Slovenian phone number"
}
]
}
As described in my previous blog post there are cases in Quarkus where you want to apply a constraint programmatically to a field. For this you also need the ConstraintDef class for your constraint:
class SloPhoneDef : ConstraintDef<SloPhoneDef, SloPhone>(SloPhone::class.java)
You can then reference it in a validator factory customizer:
@ApplicationScoped
class ContactDetailsValidatorFactoryCustomizer : ValidatorFactoryCustomizer {
override fun customize(configuration: BaseHibernateValidatorConfiguration<*>) {
val constraintMapping = configuration.createConstraintMapping()
constraintMapping
.type(ContactDetails::class.java)
.field(ContactDetails::workPhone.name)
.constraint(SloPhoneDef())
configuration.addMapping(constraintMapping)
}
}
You can find the full source code for this example in my GitHub repository.
Implementing a custom Hibernate Validator constraint requires a lot of plumbing code. In this post, I presented a sample implementation in Kotlin that follows the steps from the official Hibernate Validator documentation.