Custom validation response in Quarkus

February 3rd 2023 Quarkus

Quarkus has extensive built-in support for validation with Hibernate Validator. Simply add the hibernate-validator extension to your project:

quarkus extension add 'hibernate-validator'

Then you just need to add some annotations to your models:

class Person {
    @NotBlank
    var firstName: String? = null

    @NotBlank
    var lastName: String? = null

    var middleName: String? = null
}

and your endpoint:

@Path("/auto")
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
fun autoValidation(@Valid person: Person?): Response =
    Response.noContent().build()

This is enough for Quarkus to return a 400 response listing the detected constraint violations:

{
  "title": "Constraint Violation",
  "status": 400,
  "violations": [
    {
      "field": "autoValidation.person.firstName",
      "message": "must not be blank"
    },
    {
      "field": "autoValidation.person.lastName",
      "message": "must not be blank"
    }
  ]
}

But what if you want to modify or even localize this response?

I could not find any documentation on this, but it's quite simple if you know how: you need to implement an ExceptionMapper for the ConstraintViolationException:

class ValidationErrorDetails(
    val code: String,
    val violations: List<PropertyValidationError>)

class PropertyValidationError(
    val property: String,
    val code: String,
    val message: String)

@Provider
class ConstraintViolationMapper: ExceptionMapper<ConstraintViolationException> {
    override fun toResponse(e: ConstraintViolationException): Response {
        val errorDetails = ValidationErrorDetails(
            "validation.failed",
            e.constraintViolations.map {
                PropertyValidationError(
                    it.propertyPath.toString(),
                    it.messageTemplate,
                    it.message
                )
            }
        )
        return Response.status(400).entity(errorDetails).build()
    }
}

In my implementation above, I use the messageTemplate value as the error code for the constraint violation:

{
  "code": "validation.failed",
  "violations": [
    {
      "property": "firstName",
      "code": "{javax.validation.constraints.NotBlank.message}",
      "message": "must not be blank"
    },
    {
      "property": "lastName",
      "code": "{javax.validation.constraints.NotBlank.message}",
      "message": "must not be blank"
    }
  ]
}

You will probably want to map the values to something that makes more sense to the client. You can also use this code to look up your own error messages instead of returning the default ones.

If you implement a custom ExceptionMapper, you can also use imperative validation with the Validator class if you want more flexibility:

@Path("/manual")
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
fun manualValidation(person: Person?): Response {
    validator.validate(person).let {
        if (it.isNotEmpty()) {
            throw ConstraintViolationException(it)
        }
    }

    return Response.noContent().build()
}

Without the ExceptionMapper, you would get a 500 response because of the unhandled exception:

{
  "details": "Error id cc611017-19e2-412b-8147-00f919330fbe-1, javax.validation.ConstraintViolationException: lastName: must not be blank, firstName: must not be blank",
  "stack": "javax.validation.ConstraintViolationException: lastName: must not be blank, firstName: must not be blank\r\n\tat org.example.GreetingResource.manualValidation(GreetingResource.kt:32)\r\n\tat org.example.GreetingResource$quarkusrestinvoker$manualValidation_f2f5f5343f13a1bfd5f37014f5547729ac1b072f.invoke(Unknown Source)\r\n\tat org.jboss.resteasy.reactive.server.handlers.InvocationHandler.handle(InvocationHandler.java:29)\r\n\tat io.quarkus.resteasy.reactive.server.runtime.QuarkusResteasyReactiveRequestContext.invokeHandler(QuarkusResteasyReactiveRequestContext.java:114)\r\n\tat org.jboss.resteasy.reactive.common.core.AbstractResteasyReactiveContext.run(AbstractResteasyReactiveContext.java:145)\r\n\tat io.quarkus.vertx.core.runtime.VertxCoreRecorder$14.runWith(VertxCoreRecorder.java:576)\r\n\tat org.jboss.threads.EnhancedQueueExecutor$Task.run(EnhancedQueueExecutor.java:2449)\r\n\tat org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1478)\r\n\tat org.jboss.threads.DelegatingRunnable.run(DelegatingRunnable.java:29)\r\n\tat org.jboss.threads.ThreadLocalResettingRunnable.run(ThreadLocalResettingRunnable.java:29)\r\n\tat io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)\r\n\tat java.base/java.lang.Thread.run(Thread.java:829)"
}

With the ExceptionMapper above, both validation approaches result in an identical 400 response.

A working sample project with complete source code can be found in my GitHub repository. The last commit implements the ExceptionMapper. The previous one does not, so you can easily compare the behavior.

Although there is a lot of documentation about validation in Quarkus and even more about Hibernate Validator, I did not find any reference on how to change the Quarkus response for validation errors. I figured out how to do that by chance and have described my approach in this blog post.

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