Programmatic validation constraints in Quarkus

February 10th 2023 Quarkus

Validation in Quarkus is based on the Hibernate Validator. The support is not included by default. You need to add the hibernate-validator extension to your project:

quarkus extension add 'hibernate-validator'

Annotations are the preferred way for adding constraints:

class Customer {
    @NotNull
    var type: CustomerType? = null
    @NotNull(groups = [CustomerValidationGroups.NaturalPerson::class])
    var firstName: String? = null
    @NotNull(groups = [CustomerValidationGroups.NaturalPerson::class])
    var lastName: String? = null
    @NotNull(groups = [CustomerValidationGroups.LegalPerson::class])
    var organizationName: String? = null
}

This works well for more complex scenarios. In the example above, only the first field is validated by default. For the remaining fields, the validation group must be specified when calling the validator:

@Path("/validate")
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
fun validate(@Valid customer: Customer): Response {
    validator.validate(customer, customer.type!!.validationGroup.java).let {
        if (it.isNotEmpty()) {
            throw ConstraintViolationException(it)
        }
    }

    return Response.noContent().build()
}

In my case the validation group depends on the customer type enum:

enum class CustomerType(val validationGroup: KClass<*>) {
    NATURAL_PERSON(CustomerValidationGroups.NaturalPerson::class),
    LEGAL_PERSON(CustomerValidationGroups.LegalPerson::class)
}

Validation groups extend the Default validation group so that also the first field is always validated:

interface CustomerValidationGroups {
    interface NaturalPerson : Default
    interface LegalPerson : Default
}

Unfortunately, this annotation-based approach cannot be used if you generate your model classes from an OpenAPI specification. At best, you can have annotations for (unconditionally) required fields:

class Customer {
    @NotNull
    var type: CustomerType? = null
    var firstName: String? = null
    var lastName: String? = null
    var organizationName: String? = null
}

You need to find another way to declare the other constraints. Fortunately, you can do that in Quarkus by customizing the validator factory:

@ApplicationScoped
class CustomerValidatorFactoryCustomizer : ValidatorFactoryCustomizer {
    override fun customize(configuration: BaseHibernateValidatorConfiguration<*>) {
        val constraintMapping = configuration.createConstraintMapping()

        constraintMapping
            .type(Customer::class.java)
            .field(Customer::firstName.name)
                .constraint(NotNullDef()
                    .groups(CustomerValidationGroups.NaturalPerson::class.java))
            .field(Customer::lastName.name)
                .constraint(NotNullDef()
                    .groups(CustomerValidationGroups.NaturalPerson::class.java))
            .field(Customer::organizationName.name)
                .constraint(NotNullDef()
                    .groups(CustomerValidationGroups.LegalPerson::class.java))

        configuration.addMapping(constraintMapping)
    }
}

Of course, the code is more verbose, but the end result is the same. You can put the constraints for all classes in the same customizer if you want, but I find it more manageable by having a separate customizer for each type.

I have a working example with full source code in my GitHub repository. The last commit uses the validator factory customizer. The previous one uses annotations on the model classes, so you can easily see the differences between the two approaches.

The contract-first approach to using OpenAPI imposes some restrictions on the code that is generated from the specification, such as the model classes. Since Quarkus uses Hibernate Validator for validation, the validation constraints are expected to be provided as annotations on the model classes. Since this is not always possible for generated code, you need to find another way to achieve the same. In this blog post I have shown how a validator factory customizer can be used for this.

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