Which approach best handles field-level error reporting with Request Validation?
0 reputation · 07 Nov 2021, 18:19 UTC
0 reputation · 07 Nov 2021, 18:19 UTC
Ktor's Request Validation plugin allows for the definition of custom validation logic that triggers a RequestValidationException when constraints are violated. While this effectively prevents invalid data from reaching the routing handlers, the default exception mechanism is designed for general request rejection.
When integrating this with the StatusPages plugin, the goal is to return a structured response that identifies exactly which fields failed validation and why, rather than a generic 400 Bad Request. Since the plugin does not provide built-in annotation-based validation, developers must manually define the logic and the associated error metadata.
What is the recommended pattern for passing detailed, field-specific validation failures from the validate block through the RequestValidationException to a StatusPages handler? Does Ktor support a standard way to encapsulate multiple validation errors within a single request lifecycle?
The recommended pattern is: collect all failures inside the validate block into a structured error model, surface them through the validation exception, and let a StatusPages handler serialize that model into a 400 response. There is no annotation-based magic in Ktor's Request Validation plugin — you own both the error shape and the mapping.
Start with a serializable model, for example:
@Serializable
data class FieldError(val field: String, val message: String)
@Serializable
data class ValidationErrors(val errors: List<FieldError>)
In the validate block, accumulate every failure rather than stopping at the first one. With Ktor's ValidationResult, the idiomatic approach is to return ValidationResult.Invalid(reasons) where the reasons encode your field-level detail. When validation fails, Ktor throws a RequestValidationException carrying those reasons. One important caveat: in the Ktor versions I can speak to confidently, RequestValidationException exposes the failure detail as a list of reason strings rather than an arbitrary typed payload. If you need structured data rather than strings, two common workarounds exist:
"email: must be a valid address" and split on the delimiter in the handler — simple but brittle.class FieldValidationException(val errors: ValidationErrors) : RuntimeException()) from inside the validate block, then map that type in StatusPages. This gives you a fully typed payload.The second option is the cleaner pattern when you genuinely need a JSON array of field/message pairs, and it composes fine with Request Validation for the simple cases.
install(StatusPages) {
exception<FieldValidationException> { call, cause ->
call.respond(HttpStatusCode.BadRequest, cause.errors)
}
exception<RequestValidationException> { call, cause ->
call.respond(HttpStatusCode.BadRequest, mapOf("errors" to cause.reasons))
}
}
Because these exceptions are unchecked, they propagate through the pipeline to StatusPages — no try/catch is needed in your routes. Install both plugins at the application level; the exact property name on RequestValidationException (e.g., reasons) varies slightly across Ktor 2.x/3.x, so check the API reference for your version.
Throwable), clients get a bare 400 with no field information.exception<Throwable> or exception<Exception> mapping registered before the specific one can intercept the exception first. Register specific handlers and keep the catch-all last, or omit it.Send a request with multiple invalid fields and assert the response is 400 with a JSON array where each element contains the expected field and message. Then send a valid request to confirm the happy path is unaffected. If you see only a generic "Bad Request" body, the payload wasn't attached or a broader handler caught the exception first.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.