Skip to content

Validation

validate() evaluates all field rules together and short-circuits with a 422 response if any fail. You can validate JSON body fields, query parameters, and path parameters.

validate() is only available on HttpStream — the stream type you receive at the start of a handler. As soon as you call any transformation step (map, flatMap, blockingMap, blockingFlatMap, asyncMap, …), the stream is downgraded to an HttpMapStream, which does not expose validate().

In practice this means validation must run before any other pipeline step. It can only be used on the first step(s) of the pipeline. You may chain multiple validate() calls together, and you may combine them with requireJwt(), but once you transform the request you can no longer add validation.

// OK — validate() is the first step
return e
.validate(v -> {
v.field("name", r -> r.name).required().minLength(2);
v.field("email", r -> r.email).required().email();
})
.map(ctx -> buildUser(ctx.in()))
.complete(ctx -> HttpResult.success(ctx.in()));
// Also OK — validate() after requireJwt() (both live on HttpStream)
return e
.requireJwt(jwtProvider)
.validate(v -> v.field("name", r -> r.name).required())
.map(ctx -> buildUser(ctx.in()))
.complete(ctx -> HttpResult.success(ctx.in()));
// Does not compile — validate() is not defined on HttpMapStream
return e
.map(ctx -> ctx.in())
.validate(v -> v.field("name", r -> r.name).required()) // compile error
.complete(ctx -> HttpResult.success(ctx.in()));

Inside a validate() block, use field() for body fields, queryParam() for query string parameters, and pathParam() for path parameters:

.validate(v -> {
v.field("name", r -> r.name).required().minLength(2);
v.field("email", r -> r.email).required().email();
v.field("age", r -> r.age).required().min(0).max(150);
v.queryParam("page").required().matches("[0-9]+");
v.pathParam("userId").required().matches("[0-9]+");
})

Nested objects use a field overload that takes a validation block for the nested type. The block is only evaluated when the nested value is non-null, and error keys are prefixed with the parent field name:

v.field("address", r -> r.address, a -> {
a.field("city", x -> x.city).required();
a.field("zip", x -> x.zip).required().matches("[0-9]{5}");
});

Lists are validated with listField, which supports size constraints and per-element validation via each():

v.listField("addresses", r -> r.addresses)
.required()
.minSize(1)
.maxSize(10)
.each(a -> {
a.field("city", x -> x.city).required();
a.field("zip", x -> x.zip).required().matches("[0-9]{5}");
});

Element errors are keyed by index, e.g. addresses[0].city.

On failure the response status is 422 Unprocessable Entity and the body is:

{
"message": "Validation failed",
"errors": {
"name": ["must not be blank"],
"email": ["must be a valid email address"],
"address.zip": ["must match pattern: [0-9]{5}"]
}
}
RuleDescription
required()Must not be null or blank
minLength(n)Minimum string length
maxLength(n)Maximum string length
email()Must be a valid email address
matches(regex)Must match the given regex pattern
RuleDescription
required()Must not be null
min(n)Minimum value
max(n)Maximum value
RuleDescription
required()Must not be null
minSize(n)Minimum list size
maxSize(n)Maximum list size
each(block)Validate each element