Programmatic validation constraints in 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.