Package-level declarations
| Entry point | Role |
|---|---|
| invalidField | Inside validate<T> { }: fail with one pointer + detail pair. The pointer is a string, or a property reference the pointer is derived from. |
| invalidFields | Inside validate<T> { }: fail with more than one pointer + detail pair from the same body. Takes them as vararg pairs or as a List. |
| jsonPointer | Builds a pointer that is checked against the body type's serial descriptor, instead of written out by hand. |
| requestValidation | Inside problemDetails { }: register the RequestValidationException → Problem mapping. |
| decodeValidationReason | Reads a invalidField/invalidFields-encoded reason back into pointer + detail, for a mapping that does not go through requestValidation. |
Usage
install(RequestValidation) {
validate<Customer> { customer ->
if (customer.age > 0) ValidationResult.Valid
else invalidField("#/age", "must be a positive integer")
}
}
install(StatusPages) {
problemDetails {
requestValidation(ValidationError)
}
}When a plain exception is the better tool
This module exists to bridge RequestValidation's reason strings, which carry nothing but text, to errors[]. If you are not tied to that plugin, there is a shorter route to the same document with no encoding in between: throw your own exception from inside validate<T> { } and map it with problem-details-ktor's map<T> { }. Ktor calls validators without a try, so the exception reaches StatusPages untouched, carrying whatever typed payload you gave it.
class InvalidCustomer(val errors: List<FieldError>) : RuntimeException()
install(RequestValidation) {
validate<Customer> { customer -> if (customer.isValid) ValidationResult.Valid else throw InvalidCustomer(...) }
}
install(StatusPages) {
problemDetails {
map<InvalidCustomer> { _, cause -> /* build errors[] from cause.errors, fully typed */ }
}
}What that gives up is aggregation. The plugin runs every validator registered for a body type and concatenates their reasons; a thrown exception stops at the first one. With a single validator per type — the usual arrangement — nothing is lost, since that validator already collects its own findings before returning.
So reach for this module when you cannot leave the ValidationResult protocol: several validators on one body type, validators written elsewhere, or a codebase already returning ValidationResult.Invalid throughout. That is what it is good at, and it is a narrower claim than "the way to do validation problems in Ktor".
Pointers that cannot go stale
"#/age" written as a string is a claim about the request body that nothing checks. Rename the property and the pointer keeps naming a member that is gone. jsonPointer resolves the path against the body type's serial descriptor instead, so a path that no longer exists fails loudly:
invalidFields(
jsonPointer(Customer::age) to "must be a positive integer", // renaming `age` stops compiling
jsonPointer<Customer>("profile", "color") to "must be 'green', 'red' or 'blue'",
)
invalidField(Customer::age, "must be a positive integer") // the same, for a single fieldThe property form is checked by the compiler; the string form is checked when the pointer is built, segment by segment, and reports the members that do exist when one does not match. Segments are escaped for you — ~ and / per RFC 6901 §3, then anything a URI fragment does not admit is percent-encoded.
Two limits are worth knowing before you rely on the property form. A property renamed with @SerialName cannot be resolved from the reference — mapping a Kotlin property to its serial name needs kotlin-reflect, which this library does not use — so jsonPointer(Customer::emailAddress) throws rather than guessing, and you name it as jsonPointer<Customer>("email_address") instead. And a JsonNamingStrategy on the application's Json is invisible to the descriptor altogether; a project using one should build pointers from strings and test them against a real response.
What degrades, and how
A reason not produced by invalidField/invalidFields — a hand-written ValidationResult.Invalid("text"), or the byte-array validate { } overload, which has no field to point at — is never dropped and never throws. It becomes an errors[] entry with detail alone, no pointer.
A custom errors[] entry
requestValidation's entries are always a flat {detail, pointer} object, using RFC 9457's own example member names. For anything else — other names, extra members, a typed object — bypass it and write the RequestValidationException mapping directly, using decodeValidationReason to read what invalidField/invalidFields encoded and io.github.ilyankin.rfc9457.encodeToProblemValue (problem-details-core) to turn a typed value into a io.github.ilyankin.rfc9457.ProblemValue:
Functions
Decodes one RequestValidationException reason. This is what requestValidation does internally for every entry of errors[]. It is exposed so a mapping that needs a shape requestValidation cannot produce — a typed errors[] element, differently named members, an extra member per entry — can still read ValidationReason.pointer/ValidationReason.detail back out of a reason built by invalidField/invalidFields, instead of re-inventing the encoding.
Builds a ValidationResult.Invalid that requestValidation decodes into one errors[] entry carrying both pointer and detail.
Builds a ValidationResult.Invalid for property of the request body, deriving its pointer with jsonPointer rather than taking one written by hand. Renaming the property is then a compile error, not a pointer that quietly names a member that is gone.
The List form of invalidFields, for a validator that accumulates its findings before returning rather than knowing them all at the call site.
Builds one ValidationResult.Invalid carrying one errors[] entry per pair in fields. Use this for a validator that finds more than one invalid member in the same request body, RFC 9457's own recommended shape for multi-field validation.
Maps Ktor's RequestValidation plugin failures to type, carrying every validation reason as one entry of an errors[] extension array. This is the multi-error pattern RFC 9457 itself recommends for field-level validation failures.