Custom validation rules
You can add custom rules to the validator using the validator.rule
method. Rules should be registered only once. Hence we recommend you register them inside a service provider or a preload file
.
Throughout this guide, we will keep them inside the start/validator.ts
file. You can create this file by running the following Ace command and select the environment as "During HTTP server".
node ace make:prldfile validator
Open the newly created file and paste the following code inside it.
import { string } from '@ioc:Adonis/Core/Helpers'
import { validator } from '@ioc:Adonis/Core/Validator'
validator.rule('camelCase', (value, _, options) => {
if (typeof value !== 'string') {
return
}
if (value !== string.camelCase(value)) {
options.errorReporter.report(
options.pointer,
'camelCase',
'camelCase validation failed',
options.arrayExpressionPointer
)
}
})
- The
validator.rule
method accepts the rule name as the first argument. - The second argument is the rule implementation. The function receives the field's value under validation, the rule options, and an object representing the schema tree.
In the above example, we create a camelCase
rule that checks if the field value is the same as its camelCase version or not. If not, we will report an error using the errorReporter
class instance.
Using the rule
Before using your custom rules, you will have to inform the TypeScript compiler about the same. Otherwise, it will complain that the rule does not exist.
To inform TypeScript, we will use declaration merging
and add the property to the Rules
interface.
Create a new file at path contracts/validator.ts
(the filename is not important) and paste the following contents inside it.
declare module '@ioc:Adonis/Core/Validator' {
interface Rules {
camelCase(): Rule
}
}
Once done, you can access the camelCase
rule from the rules
object.
import { rules, schema, validator } from '@ioc:Adonis/Core/Validator'
await validator.validate({
schema: schema.create({
fileName: schema.string([
rules.camelCase()
]),
}),
data: {},
})
Passing options to the rule
Rules can also accept options, and they will be available to the validation callback as the second argument.
This time let's start from the TypeScript interface and define the options we expect from the rule consumer.
declare module '@ioc:Adonis/Core/Validator' {
interface Rules {
camelCase(maxLength?: number): Rule
}
}
All the arguments passed to the rule function are available as an array to the rule implementation. So, for example, You can access the maxLength
option as follows.
validator.rule('camelCase', (
value,
[maxLength],
options
) => {
// Rest of the validation
if (maxLength && value.length > maxLength) {
options.errorReporter.report(
options.pointer,
'camelCase.maxLength', // 👈 Keep an eye on this
'camelCase.maxLength validation failed',
options.arrayExpressionPointer,
{ maxLength }
)
}
})
Finally, if you notice, we are passing the rule name as camelCase.maxLength
to the error reporter. This will allow the users to define a custom validation message just for the maxLength
.
messages: {
'camelCase.maxLength': 'Only {{ options.maxLength }} characters are allowed'
}
Normalizing options
Many times you would want to normalize the options passed to a validation rule. For example: Using a default maxLength
when not provided by the user.
Instead of normalizing the options inside the validation callback, we recommend you normalize them only once during the compile phase.
The validator.rule
method accepts a callback function as the third argument and runs it during the compile phase.
validator.rule(
'camelCase', // rule name
() => {}, // validation callback
([maxLength]) => {
return {
compiledOptions: {
maxLength: maxLength || 10,
},
}
}
)
The compiledOptions
value is then passed to the validation callback as the second argument. As per the above example, the validation callback will receive the maxLength
as an object.
validator.rule(
'camelCase', // rule name
(value, { maxLength }) => {}, // validation callback
([maxLength]) => {
return {
compiledOptions: {
maxLength: maxLength || 10,
},
}
}
)
Async rules
To optimize the validation process, you will have to explicitly inform the validator that your validation rule is async in nature. Just return async: true
from the compile callback, and then you will be able to use async/await
inside the validation callback.
validator.rule(
'camelCase', // rule name
async () => {}, // validation callback
() => {
return {
async: true,
compiledOptions: {},
}
}
)
Restrict rules to work on a specific data type
Within the compile callback, you can access the schema type/subtype of the field on which the validation rule is applied and then conditionally allow it to be used on specific types only.
Following is an example of restricting the camelCase
rule to a string schema type only.
validator.rule(
'camelCase', // rule name
async () => {}, // validation callback
(options, type, subtype) => {
if (subtype !== 'string') {
throw new Error('"camelCase" rule can only be used with a string schema type')
}
return {
compiledOptions: {},
}
}
)
An exception will be raised if someone attempts to use the camelCase
rule on a non-string field.
schema: schema.create({
fileName: schema.number([
rules.camelCase() // will result in an error at runtime
]),
}),