Custom Hibernate constraint in Kotlin

February 17th 2023 Kotlin Quarkus

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.

Get notified when a new blog post is published (usually every Friday):

If you're looking for online one-on-one mentorship on a related topic, you can find me on Codementor.
If you need a team of experienced software engineers to help you with a project, contact us at Razum.
Copyright
Creative Commons License