AdonisJS v5 Validator: Declarative Request Validation with Schema and Custom Rules
Learn how to use AdonisJS v5's built-in Validator to define schemas, validate incoming requests, handle errors, and avoid common pitfalls.
09 Apr 2026, 12:59 UTC

Why use the built‑in Validator
AdonisJS v5 ships with a first‑party validation layer that lets you describe the shape of incoming HTTP data once, then rely on the framework to reject malformed requests before your controller logic runs. The validator compiles schemas on the first request and caches the compiled rules, so the overhead per request is minimal. Errors are automatically returned as a 422 response with a structured JSON payload, removing the need for manual if checks scattered through your actions.
Core mechanism: schema + validate helper
Validation starts by importing the Validator binding and the schema/rules factories from the IoC container. A schema is created with schema.create and can contain primitives, arrays, nested objects, optional fields, and custom rules. In a controller you call await request.validate({ schema }); if validation fails a ValidationException is thrown and the framework converts it to the 422 response.
Worked example: user registration
The following controller validates a registration payload that requires an email, a password of at least eight characters, an optional age (integer, 13‑120), and a list of at least one tag string.
import { schema, rules } from '@ioc:Adonis/Core/Validator'
import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
export default class AuthController {
public async register({ request, response }: HttpContextContract) {
const validationSchema = schema.create({
email: schema.string({}, [
rules.email(),
rules.unique({ table: 'users', column: 'email' })
]),
password: schema.string({}, [
rules.minLength(8),
rules.confirmed() // expects password_confirmation field
]),
age: schema.number.optional([
rules.range(13, 120)
]),
tags: schema.array().members(
schema.string({}, [rules.minLength(2)])
)
})
// Throws ValidationException on failure → 422 JSON response
const payload = await request.validate({ schema: validationSchema })
// At this point payload is typed and clean
const user = await User.create({
email: payload.email,
password: payload.password, // hash in a model hook or here
age: payload.age,
tags: payload.tags
})
return response.created({ user })
}
}
Key points in the example:
schema.string({}, [rules.email(), rules.unique({...})])attaches multiple rules to a single field.rules.confirmed()expects a matchingpassword_confirmationfield in the request.schema.number.optional([...])makes the field optional but validates it when present.schema.array().members(schema.string(...))validates each array element; the array itself can be further constrained withrules.minLength(1)on the array schema if you need a minimum count.
Error handling and localisation
When validation fails, AdonisJS throws a ValidationException. The default exception handler serialises it to a JSON object like:
{
"errors": [
{ "field": "email", "message": "email validation failed", "rule": "email" },
{ "field": "password", "message": "password must be at least 8 characters", "rule": "minLength" }
]
}
You can customise messages globally in config/validator.ts or per‑schema by passing a messages object to request.validate. Because the validator integrates with AdonisJS’s i18n system, you can also provide translation keys instead of hard‑coded strings.
Reusable validator classes
For complex or repeated validation logic, create a dedicated validator class using validatorFactory. This encapsulates the schema, custom messages, and even pre‑validation data transformation.
// app/Validators/RegisterValidator.ts
import { schema, rules, validatorFactory } from '@ioc:Adonis/Core/Validator'
export const RegisterValidator = validatorFactory.create((data) => {
return {
schema: schema.create({
email: schema.string({}, [rules.email(), rules.unique({ table: 'users', column: 'email' })]),
password: schema.string({}, [rules.minLength(8), rules.confirmed()]),
age: schema.number.optional([rules.range(13, 120)]),
tags: schema.array().members(schema.string({}, [rules.minLength(2)]))
}),
messages: {
'email.unique': 'This email is already registered.',
'password.minLength': 'Password must be at least 8 characters.'
}
}
})
Then in the controller:
import { RegisterValidator } from 'App/Validators/RegisterValidator'
const payload = await request.validate(RegisterValidator)
Common pitfalls and limits
Strict mode and unknown fields
By default the validator ignores fields not defined in the schema. Enabling strict: true in config/validator.ts (or per request via request.validate({ schema, strict: true })) causes any extra field to trigger a validation error. Use strict mode when you want to enforce a strict contract, but be aware that it can break clients sending additional metadata.
Custom rules performance
Custom rules (rules.custom(async (value, compiledRule) => { ... })) run for every field they are attached to. Heavy synchronous work or external API calls inside a custom rule will add latency to each request. Keep custom logic lightweight; if you need async checks (e.g., database lookups), prefer the built‑in rules.unique or rules.exists which are optimised.
Array validation nuance
Rules attached via schema.array().members(...) apply to each element. To validate the array length itself, add rules to the array schema: schema.array([rules.minLength(1), rules.maxLength(50)]).members(...).
Enum case sensitivity
schema.enum(['active', 'inactive']) matches values exactly. If the client sends Active it will fail. Normalise input (e.g., toLowerCase()) before validation or use a custom rule that does case‑insensitive comparison.
Post‑validation mutations
Do not mutate the validated payload inside the schema definition. If you need to hash a password or transform a date, do it after await request.validate() in the controller, where you have full access to the clean data.
Catching ValidationException
Only catch ValidationException if you need a custom response format. The default handler already returns a proper 422. Over‑catching can hide validation errors and make debugging harder.
Verifying the behaviour
- Create a fresh AdonisJS v5 project:
npm init adonis-ts-app@latest my-app. - Add the example controller and validator class.
- Start the server with
node ace serve --watch. - Send a POST request to
/registerwith invalid JSON (e.g., missing email). Expect a 422 response with theerrorsarray. - Send a valid payload and confirm a 201 response with the created user.
- Enable
strict: trueinconfig/validator.ts, send an extra fieldfoo: 'bar', and verify a 422 error for the unknown field.
These steps confirm that the validator compiles schemas, enforces rules, returns structured errors, and respects strict mode.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.